diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index 70d5086fd..9c54a1f96 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -19,6 +19,23 @@ jobs: - name: Check out repository uses: actions/checkout@v4 + - name: Check out bc-test-data + uses: actions/checkout@v4 + with: + repository: bcgit/bc-test-data + path: .ci-data/bc-test-data + + - name: Check out wycheproof + uses: actions/checkout@v4 + with: + repository: C2SP/wycheproof + path: .ci-data/wycheproof + + - name: Symlink test data alongside the checkout + run: | + ln -s "$GITHUB_WORKSPACE/.ci-data/bc-test-data" ../bc-test-data + ln -s "$GITHUB_WORKSPACE/.ci-data/wycheproof" ../wycheproof + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable diff --git a/Cargo.toml b/Cargo.toml index 2de6cf6ec..1c9338498 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,8 @@ bouncycastle-base64 = { path = "./crypto/base64" } bouncycastle-cipher = { path = "./crypto/cipher" } bouncycastle-core = { path = "crypto/core" } bouncycastle-core-test-framework = { path = "./crypto/core-test-framework" } +bouncycastle-ec = { path = "./crypto/ec" } +bouncycastle-ecdsa = { path = "./crypto/ecdsa" } bouncycastle-factory = { path = "./crypto/factory" } bouncycastle-hex = { path = "./crypto/hex" } bouncycastle-hkdf = { path = "./crypto/hkdf" } @@ -28,8 +30,10 @@ bouncycastle-mldsa = { path = "./crypto/mldsa" } bouncycastle-mldsa-common = { path = "./crypto/mldsa-common" } bouncycastle-mldsa-lowmemory = { path = "./crypto/mldsa-lowmemory" } bouncycastle-rng = { path = "./crypto/rng" } +bouncycastle-rsa = { path = "./crypto/rsa" } bouncycastle-sha2 = { path = "./crypto/sha2" } bouncycastle-sha3 = { path = "./crypto/sha3" } +bouncycastle-sm2 = { path = "./crypto/sm2" } bouncycastle-sm3 = { path = "./crypto/sm3" } bouncycastle-utils = { path = "./crypto/utils" } @@ -63,6 +67,7 @@ bouncycastle-ascon.workspace = true bouncycastle-base64.workspace = true bouncycastle-cipher.workspace = true bouncycastle-core.workspace = true +bouncycastle-ecdsa.workspace = true bouncycastle-factory.workspace = true bouncycastle-hex.workspace = true bouncycastle-hkdf.workspace = true @@ -72,6 +77,8 @@ bouncycastle-mldsa-lowmemory.workspace = true bouncycastle-mlkem.workspace = true bouncycastle-mlkem-lowmemory.workspace = true bouncycastle-rng.workspace = true +bouncycastle-rsa.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true +bouncycastle-sm2.workspace = true bouncycastle-sm3.workspace = true diff --git a/INTRODUCTION.md b/INTRODUCTION.md index 891af1347..9b965b3fd 100644 --- a/INTRODUCTION.md +++ b/INTRODUCTION.md @@ -257,10 +257,12 @@ the `core-interface` sub-crate. Next up will be to round out the set of cryptographic primitives: * Block ciphers (AES) -* Signatures (Ed25519, Ed448, ML-DSA, SLH-DSA) -* Key Establishment (X25519, X448, ML-KEM) +* Signatures (Ed25519, Ed448, ML-DSA, SLH-DSA, ECDSA, RSA) +* Key Establishment (X25519, X448, ML-KEM, ECDH) -(yes, you have noticed that RSA, ECDSA and ECDH are not on the list. I suppose we could, but we'd really rather not.) +(an earlier draft of this list left out RSA, ECDSA and ECDH on purpose; there has been a change of heart, and they +are being implemented on the same terms as everything else: fixed-width constant-time arithmetic, no runtime-described +parameters.) After that, we'll tackle in some kind of order (depending on public interest and funding): diff --git a/cli/src/ecdsa_cmd.rs b/cli/src/ecdsa_cmd.rs new file mode 100644 index 000000000..1ca72612d --- /dev/null +++ b/cli/src/ecdsa_cmd.rs @@ -0,0 +1,362 @@ +//! Generic CLI wiring for every ECDSA curve in `bouncycastle-ecdsa`: one instantiation of +//! [`ecdsa_cmd`] per curve, rather than the per-curve match block `mldsa_cmd.rs`/`mlkem_cmd.rs` +//! duplicate seven and three times respectively. That duplication is warranted for them -- each +//! parameter set is its own concrete algorithm with its own quirks (a hash-then-sign variant, a +//! from-seed variant, a bespoke consistency-check API) -- but every ECDSA curve implements the +//! identical `Signer`/`SignatureVerifier`/`SignaturePrivateKey`/`SignaturePublicKey`/ +//! `DerivePublicKey` trait shape with no per-curve deviation, so a single generic function bound +//! over those traits, monomorphised once per curve below, covers all seven without repeating the +//! same match block. +//! +//! Each curve has two signer types, `ECDSA*` (random `k`) and `ECDetDSA*` (RFC 6979), identical in +//! trait shape; `--deterministic` picks which one [`ecdsa_cmd`] is instantiated over. +//! +//! `ctx` (accepted by `Signer`/`SignatureVerifier` for trait conformance) is not exposed as a CLI +//! flag here: `bouncycastle_ecdsa`'s own crate docs say ECDSA "accepts but ignores" it (FIPS +//! 186-5/SEC 1 have no context-string input), so a `--ctxfile` flag would silently do nothing -- +//! every call below passes `None` directly instead. `SM2` (`sm2_cmd.rs`) is the curve this crate +//! does NOT cover: its `ctx` is mandatory (it carries the signer's identity `IDA`) and its +//! `derive_pk` is a bespoke inherent method rather than an implementation of `DerivePublicKey`, so +//! it needs its own command function and its own `--idfile` flag. + +use crate::helpers::{ + println_stdout, read_input_file, read_secret_file, read_secret_file_or_stdin, stream_stdin, + write_bytes_or_hex, +}; +use bouncycastle::core::errors::SignatureError; +use bouncycastle::core::key_material::KeyMaterialTrait; +use bouncycastle::core::signature_key::{ + DeterministicSigningKey, SignatureAlgorithm, SigningKey, VerifyingKey, +}; +use bouncycastle::core::traits::{ + RNG, SignaturePrivateKey, SignaturePublicKey, SignatureSigner, SignatureVerifier, +}; +use bouncycastle::ecdsa::keys_common::DerivePublicKey; +use bouncycastle::ecdsa::{ + ecdsa_bp256r1, ecdsa_bp256r1::ECDSABp256r1, ecdsa_bp384r1, ecdsa_bp384r1::ECDSABp384r1, + ecdsa_bp512r1, ecdsa_bp512r1::ECDSABp512r1, ecdsa_p256, ecdsa_p256::ECDSAP256, ecdsa_p256k1, + ecdsa_p256k1::ECDSASecp256K1, ecdsa_p384, ecdsa_p384::ECDSAP384, ecdsa_p521, + ecdsa_p521::ECDSAP521, +}; +use bouncycastle::ecdsa::{ + keys_bp256r1, keys_bp384r1, keys_bp512r1, keys_p256, keys_p256k1, keys_p384, keys_p521, +}; +use bouncycastle::rng::HashDRBG_SHA512; +use clap::ValueEnum; +use std::process::exit; + +#[derive(ValueEnum, Clone, Debug)] +pub(crate) enum ECDSAAction { + /// Generate and output a new private key. + Keygen, + /// Derive and output the public key matching a private key read from stdin (raw bytes, or + /// hex with --hex-input). + PkFromSk, + /// Accepts a private key (stdin) and a public key (--pkfile), and checks that they match. + CheckConsistency, + /// Sign a message read from stdin with a private key file and output the signature. + Sign, + /// Verify a message read from stdin with a public key file and a signature file. + Verify, +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn ecdsa_cmd< + A: SignatureAlgorithm, + PK: SignaturePublicKey + VerifyingKey, + SK: SignaturePrivateKey + + DerivePublicKey + + SigningKey + + DeterministicSigningKey, + const SK_LEN: usize, + const PK_LEN: usize, + const SIG_LEN: usize, +>( + action: &ECDSAAction, + keygen: fn(&mut dyn RNG) -> Result<(PK, SK), SignatureError>, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, + deterministic: bool, + alg_name: &str, +) { + match action { + ECDSAAction::Keygen => { + let (_pk, sk) = keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); + write_bytes_or_hex(&sk.encode(), output_hex); + } + ECDSAAction::PkFromSk => { + let sk = + parse_sk::(read_secret_file_or_stdin(skfile).ref_to_bytes(), alg_name); + write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); + } + ECDSAAction::CheckConsistency => { + let sk = + parse_sk::(read_secret_file_or_stdin(skfile).ref_to_bytes(), alg_name); + + let pk_bytes = require_file(pkfile, "pkfile"); + let pk = parse_pk::(&pk_bytes, alg_name); + + if sk.derive_pk() == pk { + println_stdout("SUCCESS: pk and sk match."); + } else { + eprintln!("FAILURE: pk and sk do not match."); + exit(-1); + } + } + ECDSAAction::Sign => { + let sk = + parse_sk::(require_secret(skfile, "skfile").ref_to_bytes(), alg_name); + + // stream the message from stdin; ECDSA has no context string + let sig = if deterministic { + let mut signer = sk.create_signer_deterministic(None).unwrap(); + stream_stdin(|chunk| signer.do_update(chunk)); + signer.do_final().unwrap() + } else { + let mut signer = + sk.create_signer(&mut HashDRBG_SHA512::new_from_os(), None).unwrap(); + stream_stdin(|chunk| signer.do_update(chunk)); + signer.do_final().unwrap() + }; + + write_bytes_or_hex(&sig, output_hex); + } + ECDSAAction::Verify => { + let pk_bytes = require_file(pkfile, "pkfile"); + let pk = parse_pk::(&pk_bytes, alg_name); + let sig = require_file(sigfile, "sigfile"); + + // stream the message from stdin + let mut verifier = pk.create_verifier(None).unwrap(); + stream_stdin(|chunk| verifier.do_update(chunk)); + + if verifier.do_verify_final(&sig).is_ok() { + println_stdout("Signature is valid."); + } else { + eprintln!("Signature is invalid."); + exit(-1); + } + } + } +} + +/// `--pkfile` and `--sigfile`, which cannot fall back to stdin (that carries the message). +fn require_file(file: &Option, flag_name: &str) -> Vec { + match file { + Some(f) => read_input_file(f), + None => { + eprintln!("Error: no {flag_name} provided."); + exit(-1); + } + } +} + +/// `--skfile` where signing cannot take the key from stdin. +fn require_secret(file: &Option, flag_name: &str) -> crate::helpers::SecretBytes { + match file { + Some(f) => read_secret_file(f), + None => { + eprintln!("Error: no {flag_name} provided."); + exit(-1); + } + } +} + +/// The key from its encoding; the bytes are what the file held, or its hex decoded under +/// `--hex-input`, with nothing guessed from their shape. +fn parse_sk, const SK_LEN: usize>( + bytes: &[u8], + alg_name: &str, +) -> SK { + match SK::from_bytes(bytes) { + Ok(sk) => sk, + Err(_) => { + eprintln!("Error: couldn't parse the input as a valid {alg_name} private key."); + exit(-1); + } + } +} + +/// See [`parse_sk`]. +fn parse_pk, const PK_LEN: usize>( + bytes: &[u8], + alg_name: &str, +) -> PK { + match PK::from_bytes(bytes) { + Ok(pk) => pk, + Err(_) => { + eprintln!("Error: couldn't parse the input as a valid {alg_name} public key."); + exit(-1); + } + } +} + +pub(crate) fn ecdsa_p256_cmd( + action: &ECDSAAction, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, + deterministic: bool, +) { + ecdsa_cmd::< + ECDSAP256, + _, + _, + { keys_p256::SK_LEN }, + { keys_p256::PK_LEN }, + { ecdsa_p256::SIG_LEN }, + >( + action, keys_p256::keygen, skfile, pkfile, sigfile, output_hex, deterministic, "ECDSA P-256" + ) +} + +pub(crate) fn ecdsa_p384_cmd( + action: &ECDSAAction, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, + deterministic: bool, +) { + ecdsa_cmd::< + ECDSAP384, + _, + _, + { keys_p384::SK_LEN }, + { keys_p384::PK_LEN }, + { ecdsa_p384::SIG_LEN }, + >( + action, keys_p384::keygen, skfile, pkfile, sigfile, output_hex, deterministic, "ECDSA P-384" + ) +} + +pub(crate) fn ecdsa_p521_cmd( + action: &ECDSAAction, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, + deterministic: bool, +) { + ecdsa_cmd::< + ECDSAP521, + _, + _, + { keys_p521::SK_LEN }, + { keys_p521::PK_LEN }, + { ecdsa_p521::SIG_LEN }, + >( + action, keys_p521::keygen, skfile, pkfile, sigfile, output_hex, deterministic, "ECDSA P-521" + ) +} + +pub(crate) fn ecdsa_secp256k1_cmd( + action: &ECDSAAction, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, + deterministic: bool, +) { + ecdsa_cmd::< + ECDSASecp256K1, + _, + _, + { keys_p256k1::SK_LEN }, + { keys_p256k1::PK_LEN }, + { ecdsa_p256k1::SIG_LEN }, + >( + action, + keys_p256k1::keygen, + skfile, + pkfile, + sigfile, + output_hex, + deterministic, + "ECDSA secp256k1", + ) +} + +pub(crate) fn ecdsa_bp256r1_cmd( + action: &ECDSAAction, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, + deterministic: bool, +) { + ecdsa_cmd::< + ECDSABp256r1, + _, + _, + { keys_bp256r1::SK_LEN }, + { keys_bp256r1::PK_LEN }, + { ecdsa_bp256r1::SIG_LEN }, + >( + action, + keys_bp256r1::keygen, + skfile, + pkfile, + sigfile, + output_hex, + deterministic, + "ECDSA brainpoolP256r1", + ) +} + +pub(crate) fn ecdsa_bp384r1_cmd( + action: &ECDSAAction, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, + deterministic: bool, +) { + ecdsa_cmd::< + ECDSABp384r1, + _, + _, + { keys_bp384r1::SK_LEN }, + { keys_bp384r1::PK_LEN }, + { ecdsa_bp384r1::SIG_LEN }, + >( + action, + keys_bp384r1::keygen, + skfile, + pkfile, + sigfile, + output_hex, + deterministic, + "ECDSA brainpoolP384r1", + ) +} + +pub(crate) fn ecdsa_bp512r1_cmd( + action: &ECDSAAction, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, + deterministic: bool, +) { + ecdsa_cmd::< + ECDSABp512r1, + _, + _, + { keys_bp512r1::SK_LEN }, + { keys_bp512r1::PK_LEN }, + { ecdsa_bp512r1::SIG_LEN }, + >( + action, + keys_bp512r1::keygen, + skfile, + pkfile, + sigfile, + output_hex, + deterministic, + "ECDSA brainpoolP512r1", + ) +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 289bd5b7a..66b0352c3 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -8,6 +8,7 @@ mod aes_ctr_cmd; mod aes_ecb_cmd; mod aes_gcm_cmd; mod ascon_cmd; +mod ecdsa_cmd; mod encoders_cmd; mod helpers; mod hkdf_cmd; @@ -15,16 +16,23 @@ mod mac_cmd; mod mldsa_cmd; mod mlkem_cmd; mod rng_cmd; +mod rsa_cmd; +mod rsasve_cmd; mod sha2_cmd; mod sha3_cmd; +mod sm2_cmd; mod sm3_cmd; +use crate::ecdsa_cmd::ECDSAAction; use crate::hkdf_cmd::HKDFVariant; use crate::mac_cmd::HMACVariant; use crate::mldsa_cmd::{MLDSAAction, PreHashAlg}; use crate::rng_cmd::DrbgHash; +use crate::rsa_cmd::{RSAAction, RSAHash, RSAScheme}; +use crate::rsasve_cmd::RSASVEAction; use crate::sha2_cmd::SHA2Variant; use crate::sha3_cmd::{SHA3Variant, Strength}; +use crate::sm2_cmd::SM2Action; use clap::{Parser, Subcommand}; use helpers::block_mode_helpers::CipherDirection; @@ -1008,6 +1016,196 @@ enum Subcommands { /// /// See `aes128-ecb` for the warning, the absence of an IV and the block-alignment requirement; /// only the key length differs. + /// The ECDSA signature algorithm over NIST P-256 (FIPS 186-5); `--deterministic` selects + /// Deterministic ECDSA (RFC 6979). `ctx` has no effect: ECDSA has no context-string input. + ECDSA_P256 { + action: ECDSAAction, + + #[arg(long)] + /// The private key file (in hex or binary) for signing + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, + }, + + /// The ECDSA signature algorithm over NIST P-384 (FIPS 186-5). See ecdsa-p256. + ECDSA_P384 { + action: ECDSAAction, + + #[arg(long)] + /// The private key file (in hex or binary) for signing + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, + }, + + /// The ECDSA signature algorithm over NIST P-521 (FIPS 186-5). See ecdsa-p256. + ECDSA_P521 { + action: ECDSAAction, + + #[arg(long)] + /// The private key file (in hex or binary) for signing + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, + }, + + /// The ECDSA signature algorithm over secp256k1 (SEC 2). See ecdsa-p256. + ECDSA_SECP256K1 { + action: ECDSAAction, + + #[arg(long)] + /// The private key file (in hex or binary) for signing + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, + }, + + /// The ECDSA signature algorithm over brainpoolP256r1 (RFC 5639). See ecdsa-p256. + ECDSA_BP256R1 { + action: ECDSAAction, + + #[arg(long)] + /// The private key file (in hex or binary) for signing + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, + }, + + /// The ECDSA signature algorithm over brainpoolP384r1 (RFC 5639). See ecdsa-p256. + ECDSA_BP384R1 { + action: ECDSAAction, + + #[arg(long)] + /// The private key file (in hex or binary) for signing + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, + }, + + /// The ECDSA signature algorithm over brainpoolP512r1 (RFC 5639). See ecdsa-p256. + ECDSA_BP512R1 { + action: ECDSAAction, + + #[arg(long)] + /// The private key file (in hex or binary) for signing + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, + }, + AES256_ECB { #[arg(short, long)] direction: CipherDirection, @@ -2351,6 +2549,299 @@ enum Subcommands { x: bool, }, + /// RSASVE over RSA-2048: the RSA KEM of SP 800-56B Rev. 2 SS7.2.1 (BC FIPS Java's KTS_SVE / + /// RSA-KAS-KEM, OpenSSL's "RSASVE" kem op). `keygen` (FIPS 186-5 A.1.3, e = 65537) writes the + /// private key to stdout and the public key to `--pkfile`; key files use the same raw encoding + /// as rsa-2048's. The shared secret is the raw 256-byte secret value Z, to be passed through a + /// KDF before use. + RSASVE_2048 { + action: RSASVEAction, + + #[arg(long)] + /// The private key file (in hex or binary) for decaps + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for encaps, or to write the generated public + /// key to for keygen + pkfile: Option, + + #[arg(long)] + /// The ciphertext value file (in hex or binary) either for encaps to output to, or for + /// decaps to read from. + ctfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// RSASVE over RSA-3072. See rsasve-2048. + RSASVE_3072 { + action: RSASVEAction, + + #[arg(long)] + /// The private key file (in hex or binary) for decaps + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for encaps, or to write the generated public + /// key to for keygen + pkfile: Option, + + #[arg(long)] + /// The ciphertext value file (in hex or binary) either for encaps to output to, or for + /// decaps to read from. + ctfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// RSASVE over RSA-4096. See rsasve-2048. + RSASVE_4096 { + action: RSASVEAction, + + #[arg(long)] + /// The private key file (in hex or binary) for decaps + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for encaps, or to write the generated public + /// key to for keygen + pkfile: Option, + + #[arg(long)] + /// The ciphertext value file (in hex or binary) either for encaps to output to, or for + /// decaps to read from. + ctfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// RSASVE over RSA-8192. See rsasve-2048. + RSASVE_8192 { + action: RSASVEAction, + + #[arg(long)] + /// The private key file (in hex or binary) for decaps + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for encaps, or to write the generated public + /// key to for keygen + pkfile: Option, + + #[arg(long)] + /// The ciphertext value file (in hex or binary) either for encaps to output to, or for + /// decaps to read from. + ctfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// RSA-1024 signature verification (RFC 8017 SS8.2), PKCS#1 v1.5 only. Verification-only: this + /// crate has no RSA-1024 private key type at all. See rsa-2048 for --scheme/--hash. + RSA_1024 { + #[arg(long, value_enum)] + /// The signature scheme: only `pkcs1v15` is available at this modulus size. + scheme: RSAScheme, + + #[arg(long, value_enum)] + /// The message digest: `sha256` or `sha384` only at this modulus size. + hash: RSAHash, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + }, + + /// RSA-1536 signature verification (RFC 8017 SS8.2), PKCS#1 v1.5 only. Verification-only: this + /// crate has no RSA-1536 private key type at all. See rsa-2048 for --scheme/--hash. + RSA_1536 { + #[arg(long, value_enum)] + /// The signature scheme: only `pkcs1v15` is available at this modulus size. + scheme: RSAScheme, + + #[arg(long, value_enum)] + /// The message digest: `sha256`, `sha384`, or `sha512` at this modulus size. + hash: RSAHash, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + }, + + /// RSA-2048 key generation, signing and verification: RSASSA-PKCS1-v1_5 (RFC 8017 SS8.2) or + /// RSASSA-PSS (RFC 8017 SS8.1, or RFC 8702 SS3.2.1 with `--hash shake128`), selected by + /// `--scheme`/`--hash`. PSS draws a fresh salt from the OS RNG each time it signs. `keygen` + /// (FIPS 186-5 A.1.3, e = 65537) writes the private key to stdout and the public key to + /// `--pkfile`. Private/public key files use this crate's own raw fixed-width encoding (see + /// `RsaPrivateKey`/`RsaPublicKey`'s `# Encoding` docs), not PEM or ASN.1 DER. + RSA_2048 { + action: RSAAction, + + #[arg(long, value_enum)] + /// The signature scheme. + scheme: RSAScheme, + + #[arg(long, value_enum)] + /// The message digest: `sha256`, `sha384`, `sha512`, or (PSS only) `shake128`. + hash: RSAHash, + + #[arg(long)] + /// The private key file (in hex or binary) for signing + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying, or to write the generated public + /// key to for keygen + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// RSA-3072 signing/verification. See rsa-2048. + RSA_3072 { + action: RSAAction, + + #[arg(long, value_enum)] + /// The signature scheme. + scheme: RSAScheme, + + #[arg(long, value_enum)] + /// The message digest: `sha256`, `sha384`, `sha512`, or (PSS only) `shake128`. + hash: RSAHash, + + #[arg(long)] + /// The private key file (in hex or binary) for signing + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying, or to write the generated public + /// key to for keygen + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// RSA-4096 signing/verification. See rsa-2048; note `--hash shake256` (not shake128) is the + /// SHAKE choice wired up at this modulus size (RFC 8702 SS5). + RSA_4096 { + action: RSAAction, + + #[arg(long, value_enum)] + /// The signature scheme. + scheme: RSAScheme, + + #[arg(long, value_enum)] + /// The message digest: `sha256`, `sha384`, `sha512`, or (PSS only) `shake256`. + hash: RSAHash, + + #[arg(long)] + /// The private key file (in hex or binary) for signing + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying, or to write the generated public + /// key to for keygen + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// RSA-8192 signing/verification. See rsa-2048; no PSS-SHAKE variant is wired up at this + /// modulus size (no Wycheproof vectors exist for it, and RFC 8702 SS5's "4096-bit or larger" + /// SHAKE256 recommendation is already covered by rsa-4096). + RSA_8192 { + action: RSAAction, + + #[arg(long, value_enum)] + /// The signature scheme. + scheme: RSAScheme, + + #[arg(long, value_enum)] + /// The message digest: `sha256`, `sha384`, or `sha512`. + hash: RSAHash, + + #[arg(long)] + /// The private key file (in hex or binary) for signing + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying, or to write the generated public + /// key to for keygen + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// The SM2 Digital Signature Algorithm (GB/T 32918.2-2016; draft-shen-sm2-ecdsa-02 S5), + /// randomised only (the standard specifies no deterministic scheme). `--idfile` is mandatory + /// for signing and verifying: it carries the signer's identity IDA (S5.5), which every SM2 + /// operation needs and which has no default value. + SM2 { + action: SM2Action, + + #[arg(long)] + /// The file containing the signer's identity IDA (raw bytes), mandatory for signing and + /// verifying + idfile: Option, + + #[arg(long)] + /// The private key file (in hex or binary) for signing + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for verifying + pkfile: Option, + + #[arg(long)] + /// The signature value file (in hex or binary) for verifying + sigfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// Perform SM3 of the content provided on stdin. /// Supports streaming update for low memory footprint. SM3 { @@ -2622,6 +3113,34 @@ fn run() { Some(Subcommands::Base64Encode) => { encoders_cmd::base64_encode_cmd(); } + Some(Subcommands::ECDSA_P256 { action, skfile, pkfile, sigfile, x, deterministic }) => { + ecdsa_cmd::ecdsa_p256_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); + } + Some(Subcommands::ECDSA_P384 { action, skfile, pkfile, sigfile, x, deterministic }) => { + ecdsa_cmd::ecdsa_p384_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); + } + Some(Subcommands::ECDSA_P521 { action, skfile, pkfile, sigfile, x, deterministic }) => { + ecdsa_cmd::ecdsa_p521_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); + } + Some(Subcommands::ECDSA_SECP256K1 { + action, + skfile, + pkfile, + sigfile, + x, + deterministic, + }) => { + ecdsa_cmd::ecdsa_secp256k1_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); + } + Some(Subcommands::ECDSA_BP256R1 { action, skfile, pkfile, sigfile, x, deterministic }) => { + ecdsa_cmd::ecdsa_bp256r1_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); + } + Some(Subcommands::ECDSA_BP384R1 { action, skfile, pkfile, sigfile, x, deterministic }) => { + ecdsa_cmd::ecdsa_bp384r1_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); + } + Some(Subcommands::ECDSA_BP512R1 { action, skfile, pkfile, sigfile, x, deterministic }) => { + ecdsa_cmd::ecdsa_bp512r1_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); + } Some(Subcommands::HashMLDSA44 { action, hash, @@ -2923,6 +3442,39 @@ fn run() { Some(Subcommands::SHAKE256 { length, x }) => { sha3_cmd::shake_cmd(Strength::Bits256, *length, *x); } + Some(Subcommands::RSASVE_2048 { action, skfile, pkfile, ctfile, x }) => { + rsasve_cmd::rsasve_2048_cmd(action, skfile, pkfile, ctfile, *x); + } + Some(Subcommands::RSASVE_3072 { action, skfile, pkfile, ctfile, x }) => { + rsasve_cmd::rsasve_3072_cmd(action, skfile, pkfile, ctfile, *x); + } + Some(Subcommands::RSASVE_4096 { action, skfile, pkfile, ctfile, x }) => { + rsasve_cmd::rsasve_4096_cmd(action, skfile, pkfile, ctfile, *x); + } + Some(Subcommands::RSASVE_8192 { action, skfile, pkfile, ctfile, x }) => { + rsasve_cmd::rsasve_8192_cmd(action, skfile, pkfile, ctfile, *x); + } + Some(Subcommands::RSA_1024 { scheme, hash, pkfile, sigfile }) => { + rsa_cmd::rsa_1024_cmd(scheme, hash, pkfile, sigfile); + } + Some(Subcommands::RSA_1536 { scheme, hash, pkfile, sigfile }) => { + rsa_cmd::rsa_1536_cmd(scheme, hash, pkfile, sigfile); + } + Some(Subcommands::RSA_2048 { action, scheme, hash, skfile, pkfile, sigfile, x }) => { + rsa_cmd::rsa_2048_cmd(action, scheme, hash, skfile, pkfile, sigfile, *x); + } + Some(Subcommands::RSA_3072 { action, scheme, hash, skfile, pkfile, sigfile, x }) => { + rsa_cmd::rsa_3072_cmd(action, scheme, hash, skfile, pkfile, sigfile, *x); + } + Some(Subcommands::RSA_4096 { action, scheme, hash, skfile, pkfile, sigfile, x }) => { + rsa_cmd::rsa_4096_cmd(action, scheme, hash, skfile, pkfile, sigfile, *x); + } + Some(Subcommands::RSA_8192 { action, scheme, hash, skfile, pkfile, sigfile, x }) => { + rsa_cmd::rsa_8192_cmd(action, scheme, hash, skfile, pkfile, sigfile, *x); + } + Some(Subcommands::SM2 { action, idfile, skfile, pkfile, sigfile, x }) => { + sm2_cmd::sm2_cmd(action, idfile, skfile, pkfile, sigfile, *x); + } Some(Subcommands::SM3 { x }) => { sm3_cmd::sm3_cmd(*x); } diff --git a/cli/src/rsa_cmd.rs b/cli/src/rsa_cmd.rs new file mode 100644 index 000000000..f4dfb981d --- /dev/null +++ b/cli/src/rsa_cmd.rs @@ -0,0 +1,594 @@ +//! CLI wiring for RSA signatures (RFC 8017 §8, RFC 8702 §3.2.1), one command function per modulus +//! size (`rsa_1024_cmd` .. `rsa_8192_cmd`) since modulus size is what fixes the private/public key +//! and signature byte lengths -- the same reason `ecdsa_cmd.rs` picks one command per curve. +//! Scheme (PKCS#1 v1.5 / PSS) and hash (SHA-256/384/512, or SHAKE128/256 for PSS) are +//! `--scheme`/`--hash` flags within a size's command rather than further separate subcommands: +//! unlike curve alone (ECDSA) or hash alone (HMAC) elsewhere in this CLI, RSA varies along three +//! independent axes at once, and multiplying all three out into subcommand names +//! (`RSA_2048_PSS_SHA384`, ...) would mean over thirty near-identical subcommands for what is +//! fundamentally one primitive per modulus size. +//! +//! Every (size, scheme, hash) pairing is one of `bouncycastle_rsa`'s algorithm marker +//! types (`rsa_2048::RSASSA_PKCS1_v1_5_SHA256` and siblings) that the key role traits are +//! implemented over, so, as `ecdsa_cmd.rs` does for its curves, one generic command per role +//! ([`rsa_pss_cmd`] over `SigningKey`, [`rsa_pkcs1_cmd`] over `DeterministicSigningKey`) is +//! monomorphised once per pairing below, and the message streams from stdin through the +//! workers' `do_update` in this CLI's usual ~1 KB chunks rather than being read whole. `ctx` (accepted by the traits for +//! conformance) is not exposed as a flag: RSA has no context-string input, and `bouncycastle_rsa` +//! documents the parameter as ignored, so every call below passes `None`. +//! +//! `keygen` (FIPS 186-5 Appendix A.1.3, `bouncycastle_rsa::rsa_*::keygen`) writes the private key +//! to stdout like the other key-generating commands, and -- unlike them -- also needs `--pkfile` +//! to receive the public key: an RSA private key file (RFC 8017's CRT quintuple) does not carry +//! the public exponent `e`, so there is no `PkFromSk`/`CheckConsistency` here, the public key has +//! to be kept from generation time. Private and public key files use the raw fixed-width layout +//! `RsaPrivateKey`/`RsaPublicKey`'s `SignaturePrivateKey`/`SignaturePublicKey` impls encode +//! (documented under `# Encoding` on those types): not PEM, not ASN.1 DER -- this crate has no +//! encoder for either -- so a key produced by another RSA implementation cannot be fed to this +//! CLI directly. +//! +//! 1024- and 1536-bit RSA are verification-only in `bouncycastle_rsa` (see its crate docs' `# +//! Scope`), so `rsa_1024_cmd`/`rsa_1536_cmd` take no `action`/`skfile` at all: they only verify, +//! through [`do_verify`], which needs only a `VerifyingKey` -- exactly the trait those sizes' +//! types implement. + +use crate::helpers::{ + println_stdout, read_input_file, read_secret_file, stream_stdin, write_bytes_or_hex, + write_bytes_or_hex_to_file, +}; +use bouncycastle::core::errors::SignatureError; +use bouncycastle::core::key_material::KeyMaterialTrait; +use bouncycastle::core::signature_key::{ + DeterministicSigningKey, SignatureAlgorithm, SigningKey, VerifyingKey, +}; +use bouncycastle::core::traits::{ + RNG, SignaturePrivateKey, SignaturePublicKey, SignatureSigner, SignatureVerifier, +}; +use bouncycastle::rng::HashDRBG_SHA512; +use bouncycastle::rsa::{rsa_1024, rsa_1536, rsa_2048, rsa_3072, rsa_4096, rsa_8192}; +use clap::ValueEnum; +use std::process::exit; + +#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)] +pub(crate) enum RSAAction { + /// Generate a key pair: the private key to stdout, the public key to `--pkfile`. + Keygen, + /// Sign a message read from stdin with a private key file and output the signature. + Sign, + /// Verify a message read from stdin with a public key file and a signature file. + Verify, +} + +#[derive(ValueEnum, Clone, Debug)] +pub(crate) enum RSAScheme { + /// RSASSA-PKCS1-v1_5 (RFC 8017 §8.2). + Pkcs1v15, + /// RSASSA-PSS (RFC 8017 §8.1 for SHA-256/384/512 with MGF1; RFC 8702 §3.2.1 for SHAKE128/256 + /// used natively as both the hash and the mask generation function). Salt length is fixed to + /// the hash's own output length in both cases. + Pss, +} + +#[derive(ValueEnum, Clone, Debug)] +pub(crate) enum RSAHash { + Sha256, + Sha384, + Sha512, + /// Only valid with `--scheme pss`; RFC 8702 §5 recommends this pairing for a 2048- or + /// 3072-bit modulus. + Shake128, + /// Only valid with `--scheme pss`; RFC 8702 §5 recommends this pairing for a 4096-bit or + /// larger modulus. + Shake256, +} + +/// `--pkfile` and `--sigfile`, which cannot fall back to stdin (that carries the message): raw +/// bytes, or hex under `--hex-input`. +fn require_file(file: &Option, flag_name: &str) -> Vec { + match file { + Some(f) => read_input_file(f), + None => { + eprintln!("Error: no {flag_name} provided."); + exit(-1); + } + } +} + +/// `--skfile`, into a zeroized buffer. +fn require_secret(file: &Option, flag_name: &str) -> crate::helpers::SecretBytes { + match file { + Some(f) => read_secret_file(f), + None => { + eprintln!("Error: no {flag_name} provided."); + exit(-1); + } + } +} + +fn unsupported(scheme: &RSAScheme, hash: &RSAHash, alg_name: &str) -> ! { + eprintln!("Error: {scheme:?}/{hash:?} is not supported for {alg_name}."); + exit(-1); +} + +/// See `RsaPrivateKey`'s `# Encoding` docs for the expected layout; a wrong-length file is one of +/// the `DecodingError`s `SignaturePrivateKey::from_bytes` reports. +fn parse_sk, const SK_LEN: usize>( + bytes: &[u8], + alg_name: &str, +) -> SK { + match SK::from_bytes(bytes) { + Ok(sk) => sk, + Err(_) => { + eprintln!( + "Error: couldn't parse the input as a valid {alg_name} private key (must be \ + exactly {SK_LEN} bytes)." + ); + exit(-1); + } + } +} + +/// See `RsaPublicKey`'s `# Encoding` docs for the expected layout; a wrong-length file is one of +/// the `DecodingError`s `SignaturePublicKey::from_bytes` reports. +fn parse_pk, const PK_LEN: usize>( + bytes: &[u8], + alg_name: &str, +) -> PK { + match PK::from_bytes(bytes) { + Ok(pk) => pk, + Err(_) => { + eprintln!( + "Error: couldn't parse the input as a valid {alg_name} public key (must be exactly \ + {PK_LEN} bytes)." + ); + exit(-1); + } + } +} + +/// Verifies stdin against `--pkfile`/`--sigfile` through `PK`'s streaming `VerifyingKey` +/// impl. Shared by [`rsa_sign_verify_cmd`]'s `Verify` arm, and called directly by the verify-only +/// sizes (`rsa_1024_cmd`, `rsa_1536_cmd`). A signature file of the wrong length is simply +/// invalid (RFC 8017 §8.1.2/§8.2.2 step 1), as `do_verify_final` reports it. +fn do_verify< + A: SignatureAlgorithm, + PK: SignaturePublicKey + VerifyingKey, + const PK_LEN: usize, + const SIG_LEN: usize, +>( + pkfile: &Option, + sigfile: &Option, + alg_name: &str, +) { + let pk = parse_pk::(&require_file(pkfile, "pkfile"), alg_name); + let sig = require_file(sigfile, "sigfile"); + + // `do_verify_init` is infallible for every `bouncycastle_rsa` verifier (it only stores the key + // and a fresh hash state), so this unwrap cannot fire. + let mut verifier = VerifyingKey::::create_verifier(&pk, None).unwrap(); + stream_stdin(|chunk| verifier.do_update(chunk)); + + if verifier.do_verify_final(&sig).is_ok() { + println_stdout("Signature is valid."); + } else { + eprintln!("Signature is invalid."); + exit(-1); + } +} + +/// One (size, scheme, hash) pairing `S`, generating a key pair, or signing or verifying stdin, +/// per `action`. PSS pairings draw their salt from the library's default OS-backed RNG inside +/// `S::do_sign_final`; `keygen` is the size's FIPS 186-5 generator, independent of scheme and hash. +fn rsa_keygen_cmd< + PK: SignaturePublicKey, + SK: SignaturePrivateKey, + const PK_LEN: usize, + const SK_LEN: usize, +>( + keygen: fn(&mut dyn RNG) -> Result<(PK, SK), SignatureError>, + pkfile: &Option, + output_hex: bool, + alg_name: &str, +) { + let Some(pkfile) = pkfile else { + eprintln!( + "Error: {alg_name} keygen needs --pkfile to receive the public key (an RSA \ + private key file does not carry e, so it cannot be derived later)." + ); + exit(-1); + }; + let (pk, sk) = keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap_or_else(|e| { + eprintln!("Error: {alg_name} key generation failed: {e:?}"); + exit(-1); + }); + write_bytes_or_hex_to_file(&pk.encode(), pkfile, output_hex); + write_bytes_or_hex(&sk.encode(), output_hex); +} + +/// One RSASSA-PSS pairing: `keygen` and `verify` as for every scheme; `sign` draws the salt from +/// an OS-seeded DRBG through [`SigningKey::create_signer`]. +#[allow(clippy::too_many_arguments)] +fn rsa_pss_cmd< + A: SignatureAlgorithm, + PK: SignaturePublicKey + VerifyingKey, + SK: SignaturePrivateKey + SigningKey, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +>( + action: &RSAAction, + keygen: fn(&mut dyn RNG) -> Result<(PK, SK), SignatureError>, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, + alg_name: &str, +) { + match action { + RSAAction::Keygen => { + rsa_keygen_cmd::(keygen, pkfile, output_hex, alg_name) + } + RSAAction::Sign => { + let sk = + parse_sk::(require_secret(skfile, "skfile").ref_to_bytes(), alg_name); + let mut signer = SigningKey::::create_signer( + &sk, + &mut HashDRBG_SHA512::new_from_os(), + None, + ) + .unwrap(); + stream_stdin(|chunk| signer.do_update(chunk)); + let sig = signer.do_final().unwrap_or_else(|_| { + eprintln!("Error: signing failed."); + exit(-1); + }); + write_bytes_or_hex(&sig, output_hex); + } + RSAAction::Verify => do_verify::(pkfile, sigfile, alg_name), + } +} + +/// One RSASSA-PKCS1-v1_5 pairing: as [`rsa_pss_cmd`], but the scheme is deterministic and signs +/// through [`DeterministicSigningKey::create_signer_deterministic`]. +#[allow(clippy::too_many_arguments)] +fn rsa_pkcs1_cmd< + A: SignatureAlgorithm, + PK: SignaturePublicKey + VerifyingKey, + SK: SignaturePrivateKey + DeterministicSigningKey, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +>( + action: &RSAAction, + keygen: fn(&mut dyn RNG) -> Result<(PK, SK), SignatureError>, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, + alg_name: &str, +) { + match action { + RSAAction::Keygen => { + rsa_keygen_cmd::(keygen, pkfile, output_hex, alg_name) + } + RSAAction::Sign => { + let sk = + parse_sk::(require_secret(skfile, "skfile").ref_to_bytes(), alg_name); + let mut signer = + DeterministicSigningKey::::create_signer_deterministic(&sk, None) + .unwrap(); + stream_stdin(|chunk| signer.do_update(chunk)); + let sig = signer.do_final().unwrap_or_else(|_| { + eprintln!("Error: signing failed."); + exit(-1); + }); + write_bytes_or_hex(&sig, output_hex); + } + RSAAction::Verify => do_verify::(pkfile, sigfile, alg_name), + } +} + +pub(crate) fn rsa_1024_cmd( + scheme: &RSAScheme, + hash: &RSAHash, + pkfile: &Option, + sigfile: &Option, +) { + use rsa_1024::{ + PK_LEN, RSA1024PublicKey, RSASSA_PKCS1_v1_5_SHA256, RSASSA_PKCS1_v1_5_SHA384, SIG_LEN, + }; + match (scheme, hash) { + (RSAScheme::Pkcs1v15, RSAHash::Sha256) => { + do_verify::( + pkfile, + sigfile, + "RSA-1024/PKCS#1v1.5/SHA-256", + ) + } + (RSAScheme::Pkcs1v15, RSAHash::Sha384) => { + do_verify::( + pkfile, + sigfile, + "RSA-1024/PKCS#1v1.5/SHA-384", + ) + } + (scheme, hash) => unsupported(scheme, hash, "RSA-1024"), + } +} + +/// RSA-1536: verification only (see module docs). No PSS group exists in Wycheproof at this size. +pub(crate) fn rsa_1536_cmd( + scheme: &RSAScheme, + hash: &RSAHash, + pkfile: &Option, + sigfile: &Option, +) { + use rsa_1536::{ + PK_LEN, RSA1536PublicKey, RSASSA_PKCS1_v1_5_SHA256, RSASSA_PKCS1_v1_5_SHA384, + RSASSA_PKCS1_v1_5_SHA512, SIG_LEN, + }; + match (scheme, hash) { + (RSAScheme::Pkcs1v15, RSAHash::Sha256) => { + do_verify::( + pkfile, + sigfile, + "RSA-1536/PKCS#1v1.5/SHA-256", + ) + } + (RSAScheme::Pkcs1v15, RSAHash::Sha384) => { + do_verify::( + pkfile, + sigfile, + "RSA-1536/PKCS#1v1.5/SHA-384", + ) + } + (RSAScheme::Pkcs1v15, RSAHash::Sha512) => { + do_verify::( + pkfile, + sigfile, + "RSA-1536/PKCS#1v1.5/SHA-512", + ) + } + (scheme, hash) => unsupported(scheme, hash, "RSA-1536"), + } +} + +/// RSA-2048: sign and verify. `--hash shake128` is the only SHAKE choice wired up at this size +/// (RFC 8702 §5 pairs SHAKE128 with 2048/3072-bit RSA, SHAKE256 with 4096-bit or larger). +#[allow(clippy::too_many_arguments)] +pub(crate) fn rsa_2048_cmd( + action: &RSAAction, + scheme: &RSAScheme, + hash: &RSAHash, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, +) { + use rsa_2048::{ + PK_LEN, RSA2048PrivateKey as SK, RSA2048PublicKey as PK, RSASSA_PKCS1_v1_5_SHA256, + RSASSA_PKCS1_v1_5_SHA384, RSASSA_PKCS1_v1_5_SHA512, RSASSA_PSS_SHA256, RSASSA_PSS_SHA384, + RSASSA_PSS_SHA512, RSASSA_PSS_SHAKE128, SIG_LEN, SK_LEN, + }; + let args = (skfile, pkfile, sigfile, output_hex); + let run = |name: &str, + f: fn( + &RSAAction, + fn(&mut dyn RNG) -> Result<(PK, SK), SignatureError>, + &Option, + &Option, + &Option, + bool, + &str, + )| { f(action, rsa_2048::keygen, args.0, args.1, args.2, args.3, name) }; + match (scheme, hash) { + (RSAScheme::Pkcs1v15, RSAHash::Sha256) => run( + "RSA-2048/PKCS#1v1.5/SHA-256", + rsa_pkcs1_cmd::, + ), + (RSAScheme::Pkcs1v15, RSAHash::Sha384) => run( + "RSA-2048/PKCS#1v1.5/SHA-384", + rsa_pkcs1_cmd::, + ), + (RSAScheme::Pkcs1v15, RSAHash::Sha512) => run( + "RSA-2048/PKCS#1v1.5/SHA-512", + rsa_pkcs1_cmd::, + ), + (RSAScheme::Pss, RSAHash::Sha256) => run( + "RSA-2048/PSS/SHA-256", + rsa_pss_cmd::, + ), + (RSAScheme::Pss, RSAHash::Sha384) => run( + "RSA-2048/PSS/SHA-384", + rsa_pss_cmd::, + ), + (RSAScheme::Pss, RSAHash::Sha512) => run( + "RSA-2048/PSS/SHA-512", + rsa_pss_cmd::, + ), + (RSAScheme::Pss, RSAHash::Shake128) => run( + "RSA-2048/PSS-SHAKE128", + rsa_pss_cmd::, + ), + (scheme, hash) => unsupported(scheme, hash, "RSA-2048"), + } +} + +/// RSA-3072: sign and verify. See [`rsa_2048_cmd`] for the SHAKE128 pairing rationale. +#[allow(clippy::too_many_arguments)] +pub(crate) fn rsa_3072_cmd( + action: &RSAAction, + scheme: &RSAScheme, + hash: &RSAHash, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, +) { + use rsa_3072::{ + PK_LEN, RSA3072PrivateKey as SK, RSA3072PublicKey as PK, RSASSA_PKCS1_v1_5_SHA256, + RSASSA_PKCS1_v1_5_SHA384, RSASSA_PKCS1_v1_5_SHA512, RSASSA_PSS_SHA256, RSASSA_PSS_SHA384, + RSASSA_PSS_SHA512, RSASSA_PSS_SHAKE128, SIG_LEN, SK_LEN, + }; + let args = (skfile, pkfile, sigfile, output_hex); + let run = |name: &str, + f: fn( + &RSAAction, + fn(&mut dyn RNG) -> Result<(PK, SK), SignatureError>, + &Option, + &Option, + &Option, + bool, + &str, + )| { f(action, rsa_3072::keygen, args.0, args.1, args.2, args.3, name) }; + match (scheme, hash) { + (RSAScheme::Pkcs1v15, RSAHash::Sha256) => run( + "RSA-3072/PKCS#1v1.5/SHA-256", + rsa_pkcs1_cmd::, + ), + (RSAScheme::Pkcs1v15, RSAHash::Sha384) => run( + "RSA-3072/PKCS#1v1.5/SHA-384", + rsa_pkcs1_cmd::, + ), + (RSAScheme::Pkcs1v15, RSAHash::Sha512) => run( + "RSA-3072/PKCS#1v1.5/SHA-512", + rsa_pkcs1_cmd::, + ), + (RSAScheme::Pss, RSAHash::Sha256) => run( + "RSA-3072/PSS/SHA-256", + rsa_pss_cmd::, + ), + (RSAScheme::Pss, RSAHash::Sha384) => run( + "RSA-3072/PSS/SHA-384", + rsa_pss_cmd::, + ), + (RSAScheme::Pss, RSAHash::Sha512) => run( + "RSA-3072/PSS/SHA-512", + rsa_pss_cmd::, + ), + (RSAScheme::Pss, RSAHash::Shake128) => run( + "RSA-3072/PSS-SHAKE128", + rsa_pss_cmd::, + ), + (scheme, hash) => unsupported(scheme, hash, "RSA-3072"), + } +} + +/// RSA-4096: sign and verify. `--hash shake256` (not shake128) is wired up at this size -- see +/// [`rsa_2048_cmd`] for the pairing rationale. +#[allow(clippy::too_many_arguments)] +pub(crate) fn rsa_4096_cmd( + action: &RSAAction, + scheme: &RSAScheme, + hash: &RSAHash, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, +) { + use rsa_4096::{ + PK_LEN, RSA4096PrivateKey as SK, RSA4096PublicKey as PK, RSASSA_PKCS1_v1_5_SHA256, + RSASSA_PKCS1_v1_5_SHA384, RSASSA_PKCS1_v1_5_SHA512, RSASSA_PSS_SHA256, RSASSA_PSS_SHA384, + RSASSA_PSS_SHA512, RSASSA_PSS_SHAKE256, SIG_LEN, SK_LEN, + }; + let args = (skfile, pkfile, sigfile, output_hex); + let run = |name: &str, + f: fn( + &RSAAction, + fn(&mut dyn RNG) -> Result<(PK, SK), SignatureError>, + &Option, + &Option, + &Option, + bool, + &str, + )| { f(action, rsa_4096::keygen, args.0, args.1, args.2, args.3, name) }; + match (scheme, hash) { + (RSAScheme::Pkcs1v15, RSAHash::Sha256) => run( + "RSA-4096/PKCS#1v1.5/SHA-256", + rsa_pkcs1_cmd::, + ), + (RSAScheme::Pkcs1v15, RSAHash::Sha384) => run( + "RSA-4096/PKCS#1v1.5/SHA-384", + rsa_pkcs1_cmd::, + ), + (RSAScheme::Pkcs1v15, RSAHash::Sha512) => run( + "RSA-4096/PKCS#1v1.5/SHA-512", + rsa_pkcs1_cmd::, + ), + (RSAScheme::Pss, RSAHash::Sha256) => run( + "RSA-4096/PSS/SHA-256", + rsa_pss_cmd::, + ), + (RSAScheme::Pss, RSAHash::Sha384) => run( + "RSA-4096/PSS/SHA-384", + rsa_pss_cmd::, + ), + (RSAScheme::Pss, RSAHash::Sha512) => run( + "RSA-4096/PSS/SHA-512", + rsa_pss_cmd::, + ), + (RSAScheme::Pss, RSAHash::Shake256) => run( + "RSA-4096/PSS-SHAKE256", + rsa_pss_cmd::, + ), + (scheme, hash) => unsupported(scheme, hash, "RSA-4096"), + } +} + +/// RSA-8192: sign and verify. No PSS-SHAKE variant is wired up at this size: no Wycheproof +/// vectors exist for it, and RFC 8702 §5 does not name a pairing beyond "4096-bit or larger" for +/// SHAKE256 -- 4096 already covers that recommendation. +#[allow(clippy::too_many_arguments)] +pub(crate) fn rsa_8192_cmd( + action: &RSAAction, + scheme: &RSAScheme, + hash: &RSAHash, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, +) { + use rsa_8192::{ + PK_LEN, RSA8192PrivateKey as SK, RSA8192PublicKey as PK, RSASSA_PKCS1_v1_5_SHA256, + RSASSA_PKCS1_v1_5_SHA384, RSASSA_PKCS1_v1_5_SHA512, RSASSA_PSS_SHA256, RSASSA_PSS_SHA384, + RSASSA_PSS_SHA512, SIG_LEN, SK_LEN, + }; + let args = (skfile, pkfile, sigfile, output_hex); + let run = |name: &str, + f: fn( + &RSAAction, + fn(&mut dyn RNG) -> Result<(PK, SK), SignatureError>, + &Option, + &Option, + &Option, + bool, + &str, + )| { f(action, rsa_8192::keygen, args.0, args.1, args.2, args.3, name) }; + match (scheme, hash) { + (RSAScheme::Pkcs1v15, RSAHash::Sha256) => run( + "RSA-8192/PKCS#1v1.5/SHA-256", + rsa_pkcs1_cmd::, + ), + (RSAScheme::Pkcs1v15, RSAHash::Sha384) => run( + "RSA-8192/PKCS#1v1.5/SHA-384", + rsa_pkcs1_cmd::, + ), + (RSAScheme::Pkcs1v15, RSAHash::Sha512) => run( + "RSA-8192/PKCS#1v1.5/SHA-512", + rsa_pkcs1_cmd::, + ), + (RSAScheme::Pss, RSAHash::Sha256) => run( + "RSA-8192/PSS/SHA-256", + rsa_pss_cmd::, + ), + (RSAScheme::Pss, RSAHash::Sha384) => run( + "RSA-8192/PSS/SHA-384", + rsa_pss_cmd::, + ), + (RSAScheme::Pss, RSAHash::Sha512) => run( + "RSA-8192/PSS/SHA-512", + rsa_pss_cmd::, + ), + (scheme, hash) => unsupported(scheme, hash, "RSA-8192"), + } +} diff --git a/cli/src/rsasve_cmd.rs b/cli/src/rsasve_cmd.rs new file mode 100644 index 000000000..51743d9cb --- /dev/null +++ b/cli/src/rsasve_cmd.rs @@ -0,0 +1,218 @@ +//! CLI wiring for RSASVE (SP 800-56B Rev. 2 §7.2.1), `bouncycastle_rsa`'s RSA KEM. There is one +//! command per modulus size, since the size fixes the key, ciphertext and shared-secret lengths +//! (the same split `rsa_cmd.rs` makes for signatures). A single generic [`rsasve_cmd`], bound over +//! the `KEMEncapsulator`/`KEMDecapsulator` traits, is monomorphised once per size. +//! +//! The shape follows `mlkem_cmd.rs`'s `Encaps`/`Decaps`. The shared secret always goes to stdout +//! and is never written to a file. `encaps` writes the ciphertext to `--ctfile` if one is given; +//! otherwise it prints the ciphertext and then the shared secret, both in hex, one per line. +//! `keygen` is `rsa_cmd.rs`'s: the private key to stdout, the public key to `--pkfile`. Nothing +//! else would work, because an RSA private key file does not carry `e`. +//! +//! These are separate subcommands from `rsa-` because `bouncycastle_rsa` keeps +//! key-establishment and signing keys as separate types (see its `rsasve` module's +//! `# Key separation`). The file encodings are the same raw layouts, however, and nothing here +//! stops a caller from pointing one command at the other's key files. The encodings cannot tell +//! the two kinds of key apart. +//! +//! The shared secret is RSASVE's raw secret value `Z`, the modulus's full length. It still needs +//! a KDF (e.g. `hkdf`) before it can be used as a key. + +use crate::helpers::{ + println_stdout, read_input_file_or_stdin, read_secret_file, write_bytes_or_hex, + write_bytes_or_hex_to_file, +}; +use bouncycastle::core::errors::KEMError; +use bouncycastle::core::key_material::KeyMaterialTrait; +use bouncycastle::core::traits::{ + KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, RNG, +}; +use bouncycastle::rng::HashDRBG_SHA512; +use bouncycastle::rsa::{rsa_2048, rsa_3072, rsa_4096, rsa_8192}; +use clap::ValueEnum; +use std::process::exit; + +#[derive(ValueEnum, Clone, Debug, PartialEq, Eq)] +pub(crate) enum RSASVEAction { + /// Generate a key-establishment key pair: the private key to stdout, the public key to + /// `--pkfile`. + Keygen, + /// Encapsulate to the public key in `--pkfile` (or read from stdin). With `--ctfile`, the + /// ciphertext goes to that file and the shared secret to stdout. Without it, the ciphertext + /// and then the shared secret go to stdout in hex, separated by a newline. + Encaps, + /// Decapsulate the ciphertext in `--ctfile` (or read from stdin) with the private key in + /// `--skfile`, and write the shared secret to stdout. + Decaps, +} + +/// `--skfile`, into a zeroized buffer: raw bytes, or hex under `--hex-input`. +fn require_secret(file: &Option, flag_name: &str) -> crate::helpers::SecretBytes { + match file { + Some(f) => read_secret_file(f), + None => { + eprintln!("Error: no {flag_name} provided."); + exit(-1); + } + } +} + +/// One modulus size's RSASVE, per `action`. +fn rsasve_cmd< + PK: KEMPublicKey, + SK: KEMPrivateKey, + K: KEMEncapsulator + KEMDecapsulator, + const PK_LEN: usize, + const SK_LEN: usize, + const CT_LEN: usize, + const SS_LEN: usize, +>( + action: &RSASVEAction, + keygen: fn(&mut dyn RNG) -> Result<(PK, SK), KEMError>, + skfile: &Option, + pkfile: &Option, + ctfile: &Option, + output_hex: bool, + alg_name: &str, +) { + match action { + RSASVEAction::Keygen => { + let Some(pkfile) = pkfile else { + eprintln!( + "Error: {alg_name} keygen needs --pkfile to receive the public key (an RSA \ + private key file does not carry e, so it cannot be derived later)." + ); + exit(-1); + }; + let (pk, sk) = keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap_or_else(|e| { + eprintln!("Error: {alg_name} key generation failed: {e:?}"); + exit(-1); + }); + write_bytes_or_hex_to_file(&pk.encode(), pkfile, output_hex); + write_bytes_or_hex(&sk.encode(), output_hex); + } + RSASVEAction::Encaps => { + let pk = PK::from_bytes(&read_input_file_or_stdin(pkfile)).unwrap_or_else(|e| { + eprintln!( + "Error: couldn't parse the input as a valid {alg_name} public key (must be \ + exactly {PK_LEN} bytes): {e:?}" + ); + exit(-1); + }); + let (ss, ct) = K::encaps(&pk).unwrap_or_else(|e| { + eprintln!("Error: {alg_name} encapsulation failed: {e:?}"); + exit(-1); + }); + match ctfile { + Some(ctfile) => { + write_bytes_or_hex_to_file(&ct, ctfile, output_hex); + write_bytes_or_hex(ss.ref_to_bytes(), output_hex); + } + None => { + write_bytes_or_hex(&ct, true); + println_stdout(""); + write_bytes_or_hex(ss.ref_to_bytes(), true); + } + } + } + RSASVEAction::Decaps => { + let sk = SK::from_bytes(require_secret(skfile, "skfile").ref_to_bytes()).unwrap_or_else(|e| { + eprintln!( + "Error: couldn't parse the input as a valid {alg_name} private key (must be \ + exactly {SK_LEN} bytes): {e:?}" + ); + exit(-1); + }); + // `decaps` reports a wrong-length ciphertext itself (as `LengthError`). + let ss = K::decaps(&sk, &read_input_file_or_stdin(ctfile)).unwrap_or_else(|e| { + eprintln!("Error: {alg_name} decapsulation failed: {e:?}"); + exit(-1); + }); + write_bytes_or_hex(ss.ref_to_bytes(), output_hex); + } + } +} + +pub(crate) fn rsasve_2048_cmd( + action: &RSASVEAction, + skfile: &Option, + pkfile: &Option, + ctfile: &Option, + output_hex: bool, +) { + use rsa_2048::{ + CT_LEN, PK_LEN, RSA2048KEMPrivateKey, RSA2048KEMPublicKey, RSASVE, SK_LEN, SS_LEN, + }; + rsasve_cmd::( + action, + RSASVE::keygen, + skfile, + pkfile, + ctfile, + output_hex, + "RSASVE-2048", + ); +} + +pub(crate) fn rsasve_3072_cmd( + action: &RSASVEAction, + skfile: &Option, + pkfile: &Option, + ctfile: &Option, + output_hex: bool, +) { + use rsa_3072::{ + CT_LEN, PK_LEN, RSA3072KEMPrivateKey, RSA3072KEMPublicKey, RSASVE, SK_LEN, SS_LEN, + }; + rsasve_cmd::( + action, + RSASVE::keygen, + skfile, + pkfile, + ctfile, + output_hex, + "RSASVE-3072", + ); +} + +pub(crate) fn rsasve_4096_cmd( + action: &RSASVEAction, + skfile: &Option, + pkfile: &Option, + ctfile: &Option, + output_hex: bool, +) { + use rsa_4096::{ + CT_LEN, PK_LEN, RSA4096KEMPrivateKey, RSA4096KEMPublicKey, RSASVE, SK_LEN, SS_LEN, + }; + rsasve_cmd::( + action, + RSASVE::keygen, + skfile, + pkfile, + ctfile, + output_hex, + "RSASVE-4096", + ); +} + +pub(crate) fn rsasve_8192_cmd( + action: &RSASVEAction, + skfile: &Option, + pkfile: &Option, + ctfile: &Option, + output_hex: bool, +) { + use rsa_8192::{ + CT_LEN, PK_LEN, RSA8192KEMPrivateKey, RSA8192KEMPublicKey, RSASVE, SK_LEN, SS_LEN, + }; + rsasve_cmd::( + action, + RSASVE::keygen, + skfile, + pkfile, + ctfile, + output_hex, + "RSASVE-8192", + ); +} diff --git a/cli/src/sm2_cmd.rs b/cli/src/sm2_cmd.rs new file mode 100644 index 000000000..f5857c49d --- /dev/null +++ b/cli/src/sm2_cmd.rs @@ -0,0 +1,160 @@ +//! CLI wiring for `bouncycastle-sm2`. Bespoke rather than routed through `ecdsa_cmd`'s generic +//! function: every SM2 `Signer`/`SignatureVerifier` call requires `ctx` to carry the signer's +//! identity `IDA` (GB/T 32918.2-2016 §5.5; see `bouncycastle_sm2::sm2`'s docs), whereas every +//! curve `ecdsa_cmd` covers ignores `ctx` entirely -- and `SM2PrivateKey::derive_pk` is a plain +//! inherent method rather than an implementation of `bouncycastle_ecdsa::keys_common:: +//! DerivePublicKey` (see that method's own doc comment: SM2 was never going to share a single +//! generic command function with the other seven curves regardless). `--idfile` is mandatory for +//! `Sign`/`Verify`: the crate has no default identity to fall back to when `ctx` is omitted. + +use crate::helpers::{ + println_stdout, read_input_file, read_secret_file, read_secret_file_or_stdin, stream_stdin, + write_bytes_or_hex, +}; +use bouncycastle::core::key_material::KeyMaterialTrait; +use bouncycastle::core::signature_key::{SigningKey, VerifyingKey}; +use bouncycastle::core::traits::{ + SignaturePrivateKey, SignaturePublicKey, SignatureSigner, SignatureVerifier, +}; +use bouncycastle::rng::HashDRBG_SHA512; +use bouncycastle::sm2::keys::{SM2PrivateKey, SM2PublicKey, keygen}; +use clap::ValueEnum; +use std::process::exit; + +#[derive(ValueEnum, Clone, Debug)] +pub(crate) enum SM2Action { + /// Generate and output a new private key. + Keygen, + /// Derive and output the public key matching a private key read from stdin (raw bytes, or + /// hex with --hex-input). + PkFromSk, + /// Accepts a private key (stdin) and a public key (--pkfile), and checks that they match. + CheckConsistency, + /// Sign a message read from stdin with a private key file and output the signature. + /// Requires --idfile: SM2 signing needs the signer's identity IDA, and this crate has no + /// default identity to fall back to. + Sign, + /// Verify a message read from stdin with a public key file and a signature file. + /// Requires --idfile: SM2 verification needs the signer's identity IDA, and this crate has no + /// default identity to fall back to. + Verify, +} + +pub(crate) fn sm2_cmd( + action: &SM2Action, + idfile: &Option, + skfile: &Option, + pkfile: &Option, + sigfile: &Option, + output_hex: bool, +) { + match action { + SM2Action::Keygen => { + let (_pk, sk) = keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); + write_bytes_or_hex(&sk.encode(), output_hex); + } + SM2Action::PkFromSk => { + let sk = parse_sk(read_secret_file_or_stdin(skfile).ref_to_bytes()); + write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); + } + SM2Action::CheckConsistency => { + let sk = parse_sk(read_secret_file_or_stdin(skfile).ref_to_bytes()); + + let pk_bytes = require_file(pkfile, "pkfile"); + let pk = parse_pk(&pk_bytes); + + if sk.derive_pk() == pk { + println_stdout("SUCCESS: pk and sk match."); + } else { + eprintln!("FAILURE: pk and sk do not match."); + exit(-1); + } + } + SM2Action::Sign => { + let sk = parse_sk(require_secret(skfile, "skfile").ref_to_bytes()); + let id = require_id(idfile); + + // stream the message from stdin + let mut signer = + sk.create_signer(&mut HashDRBG_SHA512::new_from_os(), Some(&id)).unwrap(); + stream_stdin(|chunk| signer.do_update(chunk)); + let sig = signer.do_final().unwrap(); + + write_bytes_or_hex(&sig, output_hex); + } + SM2Action::Verify => { + let pk_bytes = require_file(pkfile, "pkfile"); + let pk = parse_pk(&pk_bytes); + let sig = require_file(sigfile, "sigfile"); + let id = require_id(idfile); + + // stream the message from stdin + let mut verifier = pk.create_verifier(Some(&id)).unwrap(); + stream_stdin(|chunk| verifier.do_update(chunk)); + + if verifier.do_verify_final(&sig).is_ok() { + println_stdout("Signature is valid."); + } else { + eprintln!("Signature is invalid."); + exit(-1); + } + } + } +} + +fn require_file(file: &Option, flag_name: &str) -> Vec { + match file { + Some(f) => read_input_file(f), + None => { + eprintln!("Error: no {flag_name} provided."); + exit(-1); + } + } +} + +fn require_id(idfile: &Option) -> Vec { + match idfile { + Some(f) => read_input_file(f), + None => { + eprintln!( + "Error: no idfile provided. SM2 signing and verification require the signer's \ + identity IDA (GB/T 32918.2-2016 S5.5); this crate has no default identity to \ + fall back to." + ); + exit(-1); + } + } +} + +/// `--skfile` where signing cannot take the key from stdin. +fn require_secret(file: &Option, flag_name: &str) -> crate::helpers::SecretBytes { + match file { + Some(f) => read_secret_file(f), + None => { + eprintln!("Error: no {flag_name} provided."); + exit(-1); + } + } +} + +/// The key from its encoding: the file's bytes, or their hex under `--hex-input`. +fn parse_sk(bytes: &[u8]) -> SM2PrivateKey { + match SM2PrivateKey::from_bytes(bytes) { + Ok(sk) => sk, + Err(_) => { + eprintln!("Error: couldn't parse the input as a valid SM2 private key."); + exit(-1); + } + } +} + +/// See [`parse_sk`]. +fn parse_pk(bytes: &[u8]) -> SM2PublicKey { + match SM2PublicKey::from_bytes(bytes) { + Ok(pk) => pk, + Err(_) => { + eprintln!("Error: couldn't parse the input as a valid SM2 public key."); + exit(-1); + } + } +} diff --git a/cli/tests/rsa_cli_tests.rs b/cli/tests/rsa_cli_tests.rs new file mode 100644 index 000000000..1e0bd8cf0 --- /dev/null +++ b/cli/tests/rsa_cli_tests.rs @@ -0,0 +1,363 @@ +//! Tests for the `rsa-1024` .. `rsa-8192` subcommands, driving the built `bc-rust` binary as a +//! subprocess (the harness is the one `aes_cbc_cli_tests.rs` documents): the behaviour worth +//! testing is the command-line contract -- key/signature file loading, the streamed stdin message, +//! exit codes -- none of which is reachable from the library API. What the library computes is +//! cross-checked by verifying the CLI's signatures with `bouncycastle_rsa`'s own +//! `VerifyingKey` impls (and, for the deterministic PKCS#1 v1.5 case, comparing bytes). +//! +//! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the +//! current profile, so there is nothing to build or locate by hand. + +use bouncycastle::core::signature_key::{ + DeterministicSigningKey, SignatureAlgorithm, VerifyingKey, +}; +use bouncycastle::core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle::rsa::rsa_2048::{ + RSA2048PrivateKey, RSA2048PublicKey, RSASSA_PKCS1_v1_5_SHA256, RSASSA_PSS_SHA256, + RSASSA_PSS_SHAKE128, SIG_LEN, +}; +use std::io::{ErrorKind, Write}; +use std::path::PathBuf; +use std::process::{Command, Output, Stdio}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::thread; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// The genuine RSA-2048 CRT key `bouncycastle_rsa`'s own crate docs use (Wycheproof +/// `rsa_pkcs1_2048_sig_gen_test.json`'s SHA-256 group, factored as `rsa_2048_pkcs1_v1_5_tests.rs` +/// describes), in the CLI's `p || q || dP || dQ || qInv` key-file layout, hex. +const SK_2048_HEX: &str = concat!( + "dc431050f782e894fb5248247d98cb7d58b8d1e24f3b55d041c56e4de086b0d5", + "bb028bda42eeb5d234d5681e5809d415e6a289ad4cfbf78f978f6c35814f50ee", + "bff1c5b80a69f788e81e6bab5ddaa78369d659d143ec6f17e79813a575cfad9c", + "569156b90113e2e9110ad9e7b48a1c9348a6e653321191290ea36cfb3a5b18f1", + "bd1a81e7977f9898122273ae3222b598ea5fb19eb4eabc38308a5e32196603b2", + "e500ffb79f5b886816611debc472fac45544070beb057c941378a6868af3b7a0", + "3d3f9880ec47d5e089b94fbde542aba9ae8d72c57088d7abf5b131f39098f7bc", + "160f90536abc9492fd4e06f3ed7299d4b97bb03677207d95669f140cfbc20f25", + "a94b528b28f291599121d91952ffd1c7f21d7c1479d99d478885fb161870ee12", + "18bf08472612dbe5497e8d9c650688e09c786961ae3e2c354dc48ae34514759c", + "4c23c4588488961dc06b414e61c0e1e7fbbd2923d31532fe289f96da220711e5", + "8c14019808e00414276933bb07e4efb9b4a9b37656917205209f33f09515d7c1", + "3af0e72a933aef09ff2503df78bafed531c02ff1a2bc437c540cdcbd4ad35435", + "cf511763596543480629b114ca7f780ff7efa32ea0cb6e000d6d9ea1f2ef71fd", + "9cf9948422a165557e37e755edfe70d90b920502eb478bc98a63f788ce3a0f85", + "6d6ede7251a383bfa8fa480a81a925af7b3cc538c4bab8c9f7597ffb68011d8d", + "2640fbfbcfefb163ee7a87b6483a66ee41f956d90fa8a7939bfc042ee0924b1b", + "7993d0445f758d51933e85179c0320b0c968b48a91c38b5be923e1097c0c562f", + "88d42294b6a2759bafa5428a74f1270874e45f6fcc60f21602de5eccd143cf31", + "241f5921b5ad3983fb54ef17be3b285367e50c999c67247b552fe4bfce945f7b", +); +/// Its public key, `n || e` with `e = 0x10001`. +const PK_2048_HEX: &str = concat!( + "a2b451a07d0aa5f96e455671513550514a8a5b462ebef717094fa1fee82224e6", + "37f9746d3f7cafd31878d80325b6ef5a1700f65903b469429e89d6eac8845097", + "b5ab393189db92512ed8a7711a1253facd20f79c15e8247f3d3e42e46e48c98e", + "254a2fe9765313a03eff8f17e1a029397a1fa26a8dce26f490ed81299615d981", + "4c22da610428e09c7d9658594266f5c021d0fceca08d945a12be82de4d1ece6b", + "4c03145b5d3495d4ed5411eb878daf05fd7afc3e09ada0f1126422f590975a19", + "69816f48698bcbba1b4d9cae79d460d8f9f85e7975005d9bc22c4e5ac0f7c1a4", + "5d12569a62807d3b9a02e5a530e773066f453d1f5b4c2e9cf7820283f742b9d5", + "00010001", +); +/// The RSA-1024 public key from `bouncycastle_rsa`'s crate docs (Wycheproof +/// `rsa_pkcs1_1024_sig_gen_test.json`'s first group), `n || e`. +const PK_1024_HEX: &str = concat!( + "ac9048a7a4f560af91b4fcaf62a14595cb9ca9ec12000fc845e48572113cab28", + "90adb011a919575a40760d1f23fe92509c8a5810b6d05990b909dd0f4c6014f2", + "b31b6abd805bace99816e2eda41fd7b95405db7c5c8f4cf6babb14f550d5d0dd", + "5179b54951fff6aa9686f30f478db649b7c7044cc202dccad00343468eaacfbf", + "00010001", +); +/// One of that group's genuine PKCS#1 v1.5/SHA-256 signatures, over twenty zero bytes. +const SIG_1024_HEX: &str = concat!( + "41339884a9b3940e8488d666bb158063c6a2a2717cae7f564834a876fcbf7098", + "ecf3acbfabf37d38a8e6127b1e313744f1f896e165efdaea0b2e7673867842b9", + "e94db0868ed9a92bcdcb370a4e20ff275c82595e4400a8b9e9f12482f014846b", + "48216f321266ae6ae6338dbcdc41b711e483e6e3e728772e7f9f5ef95c30196b", +); + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. See +/// `aes_cbc_cli_tests.rs`'s `run` for why stdin is written from a thread and `BrokenPipe` ignored. +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"); + + let mut stdin = child.stdin.take().expect("stdin piped"); + let payload = stdin_bytes.to_vec(); + let writer = thread::spawn(move || match stdin.write_all(&payload) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + }); + + let output = child.wait_with_output().expect("failed to wait for bc-rust"); + writer.join().expect("the stdin writer thread panicked"); + output +} + +/// 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 or on `/dev/urandom`. +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() +} + +static NEXT_FILE: AtomicUsize = AtomicUsize::new(0); + +/// Writes `contents` to a fresh file under the system temp dir (unique per process and call, so +/// parallel tests never share one) and returns its path as a `String` for `--skfile` and friends. +fn temp_file(tag: &str, contents: &[u8]) -> String { + let n = NEXT_FILE.fetch_add(1, Ordering::Relaxed); + let mut path: PathBuf = std::env::temp_dir(); + path.push(format!("bc-rust-rsa-cli-{}-{n}-{tag}", std::process::id())); + std::fs::write(&path, contents).expect("write temp file"); + path.to_string_lossy().into_owned() +} + +fn sk_2048() -> RSA2048PrivateKey { + RSA2048PrivateKey::from_bytes(&unhex(SK_2048_HEX)).expect("the documented key decodes") +} + +fn pk_2048() -> RSA2048PublicKey { + RSA2048PublicKey::from_bytes(&unhex(PK_2048_HEX)).expect("the documented key decodes") +} + +/// Key and signature files are raw bytes (hex would need the global `--hex-input`). +fn rsa_2048_sign(scheme: &str, hash: &str, msg: &[u8]) -> Vec { + let skfile = temp_file("sk", &unhex(SK_2048_HEX)); + run_ok(&["rsa-2048", "sign", "--scheme", scheme, "--hash", hash, "--skfile", &skfile], msg) +} + +fn rsa_2048_verify(scheme: &str, hash: &str, msg: &[u8], sig: &[u8]) -> Output { + let pkfile = temp_file("pk", &unhex(PK_2048_HEX)); + let sigfile = temp_file("sig", sig); + run( + &[ + "rsa-2048", "verify", "--scheme", scheme, "--hash", hash, "--pkfile", &pkfile, + "--sigfile", &sigfile, + ], + msg, + ) +} + +/// Verification-only size, against a genuine Wycheproof signature: the one real known-answer +/// check the CLI can make without a key generator. +#[test] +fn rsa_1024_verifies_a_wycheproof_signature_and_rejects_another_message() { + let pkfile = temp_file("pk1024", &unhex(PK_1024_HEX)); + let sigfile = temp_file("sig1024", &unhex(SIG_1024_HEX)); + let args = [ + "rsa-1024", "--scheme", "pkcs1v15", "--hash", "sha256", "--pkfile", &pkfile, "--sigfile", + &sigfile, + ]; + let out = run_ok(&args, &[0u8; 20]); + assert!(String::from_utf8_lossy(&out).contains("Signature is valid.")); + + let err = run_err(&args, b"a message this signature was not made for"); + assert!(err.contains("Signature is invalid."), "stderr: {err}"); +} + +/// PKCS#1 v1.5 is deterministic, so the CLI's signature must equal the library's byte for byte, +/// and a payload several times the CLI's 1 KB stream buffer proves the message is hashed across +/// chunk boundaries correctly. +#[test] +fn rsa_2048_pkcs1v15_signature_matches_the_library_and_verifies() { + let msg = pseudo_random(5000, 7); + let sig = rsa_2048_sign("pkcs1v15", "sha256", &msg); + let expected = DeterministicSigningKey::::sign_deterministic( + &sk_2048(), + &msg, + None, + ) + .unwrap(); + assert_eq!(sig, expected.to_vec()); + + let out = rsa_2048_verify("pkcs1v15", "sha256", &msg, &sig); + assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); + assert!(String::from_utf8_lossy(&out.stdout).contains("Signature is valid.")); + + // `-x` writes the same signature as hex. + let skfile = temp_file("sk", &unhex(SK_2048_HEX)); + let hex = run_ok( + &[ + "rsa-2048", "sign", "--scheme", "pkcs1v15", "--hash", "sha256", "--skfile", &skfile, + "-x", + ], + &msg, + ); + assert_eq!(unhex(String::from_utf8_lossy(&hex).trim()), sig); +} + +/// One PSS pairing: two CLI signatures of one message differ (a fresh salt each), and both +/// verify -- through the library's `VerifyingKey` impl for `PK` and through the CLI. +fn pss_case(hash: &str, msg: &[u8]) +where + RSA2048PublicKey: VerifyingKey, +{ + let pk = pk_2048(); + let sig_a = rsa_2048_sign("pss", hash, msg); + let sig_b = rsa_2048_sign("pss", hash, msg); + assert_ne!(sig_a, sig_b, "PSS/{hash}: a fresh salt each time"); + for sig in [&sig_a, &sig_b] { + VerifyingKey::::verify(&pk, msg, None, sig) + .unwrap_or_else(|e| panic!("PSS/{hash}: library rejected {e:?}")); + let out = rsa_2048_verify("pss", hash, msg, sig); + assert!(out.status.success(), "PSS/{hash}: {}", String::from_utf8_lossy(&out.stderr)); + } +} + +/// PSS draws a fresh salt per signature: see [`pss_case`], for the MGF1/SHA-256 and the +/// SHAKE128-native pairings. +#[test] +fn rsa_2048_pss_signatures_are_fresh_and_verify_both_ways() { + let msg = pseudo_random(3000, 11); + pss_case::("sha256", &msg); + pss_case::("shake128", &msg); +} + +#[test] +fn rsa_2048_verify_rejects_a_tampered_signature_and_a_truncated_one() { + let msg = b"tamper me"; + let mut sig = rsa_2048_sign("pkcs1v15", "sha256", msg); + sig[17] ^= 0x01; + let out = rsa_2048_verify("pkcs1v15", "sha256", msg, &sig); + assert!(!out.status.success()); + assert!(String::from_utf8_lossy(&out.stderr).contains("Signature is invalid.")); + + // RFC 8017 §8.2.2 step 1: a signature that is not k octets is invalid. + sig[17] ^= 0x01; + let out = rsa_2048_verify("pkcs1v15", "sha256", msg, &sig[..255]); + assert!(!out.status.success()); + assert!(String::from_utf8_lossy(&out.stderr).contains("Signature is invalid.")); +} + +#[test] +fn rsa_2048_rejects_a_wrong_length_key_file() { + let short_sk = temp_file("shortsk", &unhex(SK_2048_HEX)[..639]); + let err = run_err( + &["rsa-2048", "sign", "--scheme", "pkcs1v15", "--hash", "sha256", "--skfile", &short_sk], + b"msg", + ); + assert!(err.contains("640 bytes"), "stderr: {err}"); + + let short_pk = temp_file("shortpk", &unhex(PK_2048_HEX)[..259]); + let sigfile = temp_file("sig", &[0u8; 256]); + let err = run_err( + &[ + "rsa-2048", "verify", "--scheme", "pkcs1v15", "--hash", "sha256", "--pkfile", + &short_pk, "--sigfile", &sigfile, + ], + b"msg", + ); + assert!(err.contains("260 bytes"), "stderr: {err}"); +} + +/// Pairings a size does not offer (PSS at 1024 bits, SHAKE256 at 2048) are refused up front. +#[test] +fn unsupported_pairings_are_refused() { + let pkfile = temp_file("pk1024", &unhex(PK_1024_HEX)); + let sigfile = temp_file("sig1024", &unhex(SIG_1024_HEX)); + let err = run_err( + &[ + "rsa-1024", "--scheme", "pss", "--hash", "sha256", "--pkfile", &pkfile, "--sigfile", + &sigfile, + ], + &[0u8; 20], + ); + assert!(err.contains("not supported"), "stderr: {err}"); + + let skfile = temp_file("sk", &unhex(SK_2048_HEX)); + let err = run_err( + &["rsa-2048", "sign", "--scheme", "pss", "--hash", "shake256", "--skfile", &skfile], + b"msg", + ); + assert!(err.contains("not supported"), "stderr: {err}"); +} + +/// `keygen` writes a 640-byte private key to stdout and a 260-byte public key to `--pkfile`, and +/// the pair then signs and verifies through the CLI; without `--pkfile` it refuses, since the +/// public key cannot be recovered from the private key file later. +#[test] +fn rsa_2048_keygen_then_sign_and_verify() { + let pkfile = temp_file("genpk", b""); + let sk_hex = run_ok( + &[ + "rsa-2048", "keygen", "--scheme", "pkcs1v15", "--hash", "sha256", "--pkfile", &pkfile, + "-x", + ], + &[], + ); + let sk_hex = String::from_utf8(sk_hex).unwrap(); + assert_eq!(sk_hex.trim().len(), 2 * 640, "private key on stdout, hex"); + let pk_hex = std::fs::read_to_string(&pkfile).unwrap(); + assert_eq!(pk_hex.trim().len(), 2 * 260, "public key in --pkfile, hex"); + let pk = RSA2048PublicKey::from_bytes(&unhex(pk_hex.trim())).unwrap(); + assert_eq!(pk.e(), 65537); + + // The keys came out in hex (`-x`); the files a command reads are raw bytes. + let skfile = temp_file("gensk", &unhex(sk_hex.trim())); + let pkfile = temp_file("genpkraw", &unhex(pk_hex.trim())); + let msg = pseudo_random(2000, 5); + let sig = run_ok( + &["rsa-2048", "sign", "--scheme", "pss", "--hash", "sha256", "--skfile", &skfile], + &msg, + ); + let sigfile = temp_file("gensig", &sig); + let out = run_ok( + &[ + "rsa-2048", "verify", "--scheme", "pss", "--hash", "sha256", "--pkfile", &pkfile, + "--sigfile", &sigfile, + ], + &msg, + ); + assert!(String::from_utf8_lossy(&out).contains("Signature is valid.")); + VerifyingKey::::verify(&pk, &msg, None, &sig).unwrap(); + + let err = run_err(&["rsa-2048", "keygen", "--scheme", "pkcs1v15", "--hash", "sha256"], &[]); + assert!(err.contains("--pkfile"), "stderr: {err}"); +} diff --git a/crypto/core-test-framework/src/signature.rs b/crypto/core-test-framework/src/signature.rs index 11c073b71..f60251f17 100644 --- a/crypto/core-test-framework/src/signature.rs +++ b/crypto/core-test-framework/src/signature.rs @@ -136,7 +136,10 @@ impl TestFrameworkSignature { // streaming signer, in one chunk and in many, equals the one-shot let sig = sk.sign_deterministic(DUMMY_SEED, Some(b"streaming")).unwrap(); let mut signer = sk.create_signer_deterministic(Some(b"streaming")).unwrap(); - assert_eq!(signer.alg_name(), A::ALG_NAME); + // A deterministic variant may name how it picks its nonce (`ECDetDSA-P256` for RFC 6979 + // under the `ECDSAP256` marker), so only the strength is pinned to the marker here. + assert!(!signer.alg_name().is_empty()); + assert_eq!(signer.max_security_strength(), A::MAX_SECURITY_STRENGTH); signer.do_update(DUMMY_SEED); assert_eq!(signer.do_final().unwrap(), sig); @@ -278,7 +281,10 @@ impl TestFrameworkSignature { let sig = sk.sign_ph_deterministic::(&ph, Some(b"ph")).unwrap(); assert_eq!(sig, sk.sign_ph_deterministic::(&ph, Some(b"ph")).unwrap()); let mut signer = sk.create_ph_signer_deterministic::(Some(b"ph")).unwrap(); - assert_eq!(signer.alg_name(), A::ALG_NAME); + // A deterministic variant may name how it picks its nonce (`ECDetDSA-P256` for RFC 6979 + // under the `ECDSAP256` marker), so only the strength is pinned to the marker here. + assert!(!signer.alg_name().is_empty()); + assert_eq!(signer.max_security_strength(), A::MAX_SECURITY_STRENGTH); for chunk in DUMMY_SEED.chunks(100) { signer.do_update(chunk); } diff --git a/crypto/ec/Cargo.toml b/crypto/ec/Cargo.toml new file mode 100644 index 000000000..bd34b6de7 --- /dev/null +++ b/crypto/ec/Cargo.toml @@ -0,0 +1,107 @@ +[package] +name = "bouncycastle-ec" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-utils.workspace = true + +[dev-dependencies] +bouncycastle-hex.workspace = true +criterion.workspace = true + +[[bench]] +name = "p256_field_benches" +harness = false + +[[bench]] +name = "p256_point_benches" +harness = false + +[[bench]] +name = "p384_field_benches" +harness = false + +[[bench]] +name = "p384_point_benches" +harness = false + +[[bench]] +name = "p521_field_benches" +harness = false + +[[bench]] +name = "p521_point_benches" +harness = false + +[[bench]] +name = "p256k1_field_benches" +harness = false + +[[bench]] +name = "p256k1_point_benches" +harness = false + +[[bench]] +name = "bp256r1_field_benches" +harness = false + +[[bench]] +name = "bp256r1_point_benches" +harness = false + +[[bench]] +name = "bp384r1_field_benches" +harness = false + +[[bench]] +name = "bp384r1_point_benches" +harness = false + +[[bench]] +name = "bp512r1_field_benches" +harness = false + +[[bench]] +name = "bp512r1_point_benches" +harness = false + +[[bench]] +name = "sm2_field_benches" +harness = false + +[[bench]] +name = "sm2_point_benches" +harness = false + +[[bench]] +name = "p256_scalar_benches" +harness = false + +[[bench]] +name = "p384_scalar_benches" +harness = false + +[[bench]] +name = "p521_scalar_benches" +harness = false + +[[bench]] +name = "p256k1_scalar_benches" +harness = false + +[[bench]] +name = "bp256r1_scalar_benches" +harness = false + +[[bench]] +name = "bp384r1_scalar_benches" +harness = false + +[[bench]] +name = "bp512r1_scalar_benches" +harness = false + +[[bench]] +name = "sm2_scalar_benches" +harness = false diff --git a/crypto/ec/benches/bp256r1_field_benches.rs b/crypto/ec/benches/bp256r1_field_benches.rs new file mode 100644 index 000000000..e108c911e --- /dev/null +++ b/crypto/ec/benches/bp256r1_field_benches.rs @@ -0,0 +1,24 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::bp256r1::Bp256r1FieldElement; +use bouncycastle_ec::bp256r1_domain::{G_X_LIMBS, G_Y_LIMBS}; + +fn bench_bp256r1_field(c: &mut Criterion) { + let a = Bp256r1FieldElement::from_limbs(G_X_LIMBS); + let b = Bp256r1FieldElement::from_limbs(G_Y_LIMBS); + + let mut group = c.benchmark_group("bp256r1_field"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(a).add(black_box(&b)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(a).mul(black_box(&b)))) + }); + group.bench_function("square", |bencher| bencher.iter(|| black_box(black_box(a).square()))); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(a).invert()))); + group.finish(); +} + +criterion_group!(benches, bench_bp256r1_field); +criterion_main!(benches); diff --git a/crypto/ec/benches/bp256r1_point_benches.rs b/crypto/ec/benches/bp256r1_point_benches.rs new file mode 100644 index 000000000..f2eb37985 --- /dev/null +++ b/crypto/ec/benches/bp256r1_point_benches.rs @@ -0,0 +1,38 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::bp256r1::Bp256r1FieldElement; +use bouncycastle_ec::bp256r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp256r1_mul::multiply_point; +use bouncycastle_ec::bp256r1_point::Bp256r1JacobianPoint; +use bouncycastle_ec::bp256r1_scalar::Bp256r1Scalar; + +fn bench_bp256r1_point(c: &mut Criterion) { + let g = Bp256r1JacobianPoint::from_affine( + Bp256r1FieldElement::from_limbs(G_X_LIMBS), + Bp256r1FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + + let mut group = c.benchmark_group("bp256r1_point"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(&g).add(black_box(&two_g)))) + }); + group.bench_function("double", |bencher| bencher.iter(|| black_box(black_box(&g).double()))); + group.bench_function("to_affine", |bencher| { + bencher.iter(|| black_box(black_box(&two_g).to_affine())) + }); + // A full-width scalar: the multiplier's cost does not depend on its value. + let mut k_limbs = [0u64; 4]; + for (i, limb) in k_limbs.iter_mut().enumerate() { + *limb = 0x9E37_79B9_7F4A_7C15 ^ (i as u64); + } + let k = Bp256r1Scalar::from_limbs(k_limbs); + group.bench_function("multiply_point", |bencher| { + bencher.iter(|| black_box(multiply_point(black_box(&k), black_box(&two_g)))) + }); + group.finish(); +} + +criterion_group!(benches, bench_bp256r1_point); +criterion_main!(benches); diff --git a/crypto/ec/benches/bp256r1_scalar_benches.rs b/crypto/ec/benches/bp256r1_scalar_benches.rs new file mode 100644 index 000000000..d5b8f8ec2 --- /dev/null +++ b/crypto/ec/benches/bp256r1_scalar_benches.rs @@ -0,0 +1,68 @@ +//! Benchmarks for brainpoolP256r1's scalar side: the scalar field (arithmetic mod the +//! curve order `n`, used for `k^-1` and `s = k^-1(e + rd)` in signing) and the two scalar +//! multipliers built on it -- [`comb_multiply_base_point`], the constant-time fixed-base `[k]G` +//! that dominates key generation and signing, and [`shamir_multiply`], the variable-time `[u]G + +//! [v]Q` that dominates verification. These are the crate's most expensive routines; the field and +//! point benches next to this file measure only their building blocks. + +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::bp256r1::Bp256r1FieldElement; +use bouncycastle_ec::bp256r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp256r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp256r1_point::Bp256r1JacobianPoint; +use bouncycastle_ec::bp256r1_scalar::{Bp256r1PublicScalar, Bp256r1Scalar, Bp256r1ScalarField}; +use bouncycastle_ec::bp256r1_wnaf::shamir_multiply; + +const A_LIMBS: [u64; 4] = + [0x9f8573c9f25dc994, 0x9115361f42389a31, 0xfc83ab74842a0944, 0x285f078965f5a299]; +const B_LIMBS: [u64; 4] = + [0x790d02ba68e2b293, 0x952cb98dca28e0ce, 0x1699fd1dd3e61f5f, 0x174ea8e941ac9159]; + +fn bench_bp256r1_scalar(c: &mut Criterion) { + let x = Bp256r1ScalarField::from_limbs(A_LIMBS); + let y = Bp256r1ScalarField::from_limbs(B_LIMBS); + + let mut group = c.benchmark_group("bp256r1_scalar"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(x).add(black_box(&y)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(x).mul(black_box(&y)))) + }); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(x).invert()))); + // Variable-time inverse on the public scalar type (verification's `s^-1`). + let x_public = Bp256r1PublicScalar::from_limbs(A_LIMBS); + group.bench_function("invert_vartime", |bencher| { + bencher.iter(|| black_box(black_box(x_public).invert_vartime())) + }); + group.finish(); +} + +fn bench_bp256r1_scalar_mul(c: &mut Criterion) { + let k = Bp256r1Scalar::from_limbs(A_LIMBS); + let u = Bp256r1PublicScalar::from_limbs(A_LIMBS); + let v = Bp256r1PublicScalar::from_limbs(B_LIMBS); + // A stand-in for a signer's public key: any point on the curve will do for timing, and `[2]G` + // is reachable without a scalar multiplication of its own. + let g = Bp256r1JacobianPoint::from_affine( + Bp256r1FieldElement::from_limbs(G_X_LIMBS), + Bp256r1FieldElement::from_limbs(G_Y_LIMBS), + ); + let q = g.double(); + + let mut group = c.benchmark_group("bp256r1_scalar_mul"); + // Constant-time fixed-base [k]G (signing, key generation). + group.bench_function("comb_base_point", |bencher| { + bencher.iter(|| black_box(comb_multiply_base_point(black_box(&k)))) + }); + // Variable-time [u]G + [v]Q via Shamir's trick (verification). + group.bench_function("shamir", |bencher| { + bencher.iter(|| shamir_multiply(black_box(&u), black_box(&v), black_box(&q))) + }); + group.finish(); +} + +criterion_group!(benches, bench_bp256r1_scalar, bench_bp256r1_scalar_mul); +criterion_main!(benches); diff --git a/crypto/ec/benches/bp384r1_field_benches.rs b/crypto/ec/benches/bp384r1_field_benches.rs new file mode 100644 index 000000000..65c551cd7 --- /dev/null +++ b/crypto/ec/benches/bp384r1_field_benches.rs @@ -0,0 +1,24 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::bp384r1::Bp384r1FieldElement; +use bouncycastle_ec::bp384r1_domain::{G_X_LIMBS, G_Y_LIMBS}; + +fn bench_bp384r1_field(c: &mut Criterion) { + let a = Bp384r1FieldElement::from_limbs(G_X_LIMBS); + let b = Bp384r1FieldElement::from_limbs(G_Y_LIMBS); + + let mut group = c.benchmark_group("bp384r1_field"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(a).add(black_box(&b)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(a).mul(black_box(&b)))) + }); + group.bench_function("square", |bencher| bencher.iter(|| black_box(black_box(a).square()))); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(a).invert()))); + group.finish(); +} + +criterion_group!(benches, bench_bp384r1_field); +criterion_main!(benches); diff --git a/crypto/ec/benches/bp384r1_point_benches.rs b/crypto/ec/benches/bp384r1_point_benches.rs new file mode 100644 index 000000000..c2e16c4c6 --- /dev/null +++ b/crypto/ec/benches/bp384r1_point_benches.rs @@ -0,0 +1,38 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::bp384r1::Bp384r1FieldElement; +use bouncycastle_ec::bp384r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp384r1_mul::multiply_point; +use bouncycastle_ec::bp384r1_point::Bp384r1JacobianPoint; +use bouncycastle_ec::bp384r1_scalar::Bp384r1Scalar; + +fn bench_bp384r1_point(c: &mut Criterion) { + let g = Bp384r1JacobianPoint::from_affine( + Bp384r1FieldElement::from_limbs(G_X_LIMBS), + Bp384r1FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + + let mut group = c.benchmark_group("bp384r1_point"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(&g).add(black_box(&two_g)))) + }); + group.bench_function("double", |bencher| bencher.iter(|| black_box(black_box(&g).double()))); + group.bench_function("to_affine", |bencher| { + bencher.iter(|| black_box(black_box(&two_g).to_affine())) + }); + // A full-width scalar: the multiplier's cost does not depend on its value. + let mut k_limbs = [0u64; 6]; + for (i, limb) in k_limbs.iter_mut().enumerate() { + *limb = 0x9E37_79B9_7F4A_7C15 ^ (i as u64); + } + let k = Bp384r1Scalar::from_limbs(k_limbs); + group.bench_function("multiply_point", |bencher| { + bencher.iter(|| black_box(multiply_point(black_box(&k), black_box(&two_g)))) + }); + group.finish(); +} + +criterion_group!(benches, bench_bp384r1_point); +criterion_main!(benches); diff --git a/crypto/ec/benches/bp384r1_scalar_benches.rs b/crypto/ec/benches/bp384r1_scalar_benches.rs new file mode 100644 index 000000000..431bf973b --- /dev/null +++ b/crypto/ec/benches/bp384r1_scalar_benches.rs @@ -0,0 +1,72 @@ +//! Benchmarks for brainpoolP384r1's scalar side: the scalar field (arithmetic mod the +//! curve order `n`, used for `k^-1` and `s = k^-1(e + rd)` in signing) and the two scalar +//! multipliers built on it -- [`comb_multiply_base_point`], the constant-time fixed-base `[k]G` +//! that dominates key generation and signing, and [`shamir_multiply`], the variable-time `[u]G + +//! [v]Q` that dominates verification. These are the crate's most expensive routines; the field and +//! point benches next to this file measure only their building blocks. + +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::bp384r1::Bp384r1FieldElement; +use bouncycastle_ec::bp384r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp384r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp384r1_point::Bp384r1JacobianPoint; +use bouncycastle_ec::bp384r1_scalar::{Bp384r1PublicScalar, Bp384r1Scalar, Bp384r1ScalarField}; +use bouncycastle_ec::bp384r1_wnaf::shamir_multiply; + +const A_LIMBS: [u64; 6] = [ + 0x9f8573c9f25dc994, 0x9115361f42389a31, 0xfc83ab74842a0944, 0x285f078965f5a299, + 0x3d7c9ec7081ab44e, 0xbd8ec9a1f80385ed, +]; +const B_LIMBS: [u64; 6] = [ + 0x790d02ba68e2b293, 0x952cb98dca28e0ce, 0x1699fd1dd3e61f5f, 0x174ea8e941ac9159, + 0xc59a61378b3dda8e, 0x083d8f37af9e4e0d, +]; + +fn bench_bp384r1_scalar(c: &mut Criterion) { + let x = Bp384r1ScalarField::from_limbs(A_LIMBS); + let y = Bp384r1ScalarField::from_limbs(B_LIMBS); + + let mut group = c.benchmark_group("bp384r1_scalar"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(x).add(black_box(&y)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(x).mul(black_box(&y)))) + }); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(x).invert()))); + // Variable-time inverse on the public scalar type (verification's `s^-1`). + let x_public = Bp384r1PublicScalar::from_limbs(A_LIMBS); + group.bench_function("invert_vartime", |bencher| { + bencher.iter(|| black_box(black_box(x_public).invert_vartime())) + }); + group.finish(); +} + +fn bench_bp384r1_scalar_mul(c: &mut Criterion) { + let k = Bp384r1Scalar::from_limbs(A_LIMBS); + let u = Bp384r1PublicScalar::from_limbs(A_LIMBS); + let v = Bp384r1PublicScalar::from_limbs(B_LIMBS); + // A stand-in for a signer's public key: any point on the curve will do for timing, and `[2]G` + // is reachable without a scalar multiplication of its own. + let g = Bp384r1JacobianPoint::from_affine( + Bp384r1FieldElement::from_limbs(G_X_LIMBS), + Bp384r1FieldElement::from_limbs(G_Y_LIMBS), + ); + let q = g.double(); + + let mut group = c.benchmark_group("bp384r1_scalar_mul"); + // Constant-time fixed-base [k]G (signing, key generation). + group.bench_function("comb_base_point", |bencher| { + bencher.iter(|| black_box(comb_multiply_base_point(black_box(&k)))) + }); + // Variable-time [u]G + [v]Q via Shamir's trick (verification). + group.bench_function("shamir", |bencher| { + bencher.iter(|| shamir_multiply(black_box(&u), black_box(&v), black_box(&q))) + }); + group.finish(); +} + +criterion_group!(benches, bench_bp384r1_scalar, bench_bp384r1_scalar_mul); +criterion_main!(benches); diff --git a/crypto/ec/benches/bp512r1_field_benches.rs b/crypto/ec/benches/bp512r1_field_benches.rs new file mode 100644 index 000000000..88c7d404e --- /dev/null +++ b/crypto/ec/benches/bp512r1_field_benches.rs @@ -0,0 +1,24 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::bp512r1::Bp512r1FieldElement; +use bouncycastle_ec::bp512r1_domain::{G_X_LIMBS, G_Y_LIMBS}; + +fn bench_bp512r1_field(c: &mut Criterion) { + let a = Bp512r1FieldElement::from_limbs(G_X_LIMBS); + let b = Bp512r1FieldElement::from_limbs(G_Y_LIMBS); + + let mut group = c.benchmark_group("bp512r1_field"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(a).add(black_box(&b)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(a).mul(black_box(&b)))) + }); + group.bench_function("square", |bencher| bencher.iter(|| black_box(black_box(a).square()))); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(a).invert()))); + group.finish(); +} + +criterion_group!(benches, bench_bp512r1_field); +criterion_main!(benches); diff --git a/crypto/ec/benches/bp512r1_point_benches.rs b/crypto/ec/benches/bp512r1_point_benches.rs new file mode 100644 index 000000000..bdec293f6 --- /dev/null +++ b/crypto/ec/benches/bp512r1_point_benches.rs @@ -0,0 +1,38 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::bp512r1::Bp512r1FieldElement; +use bouncycastle_ec::bp512r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp512r1_mul::multiply_point; +use bouncycastle_ec::bp512r1_point::Bp512r1JacobianPoint; +use bouncycastle_ec::bp512r1_scalar::Bp512r1Scalar; + +fn bench_bp512r1_point(c: &mut Criterion) { + let g = Bp512r1JacobianPoint::from_affine( + Bp512r1FieldElement::from_limbs(G_X_LIMBS), + Bp512r1FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + + let mut group = c.benchmark_group("bp512r1_point"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(&g).add(black_box(&two_g)))) + }); + group.bench_function("double", |bencher| bencher.iter(|| black_box(black_box(&g).double()))); + group.bench_function("to_affine", |bencher| { + bencher.iter(|| black_box(black_box(&two_g).to_affine())) + }); + // A full-width scalar: the multiplier's cost does not depend on its value. + let mut k_limbs = [0u64; 8]; + for (i, limb) in k_limbs.iter_mut().enumerate() { + *limb = 0x9E37_79B9_7F4A_7C15 ^ (i as u64); + } + let k = Bp512r1Scalar::from_limbs(k_limbs); + group.bench_function("multiply_point", |bencher| { + bencher.iter(|| black_box(multiply_point(black_box(&k), black_box(&two_g)))) + }); + group.finish(); +} + +criterion_group!(benches, bench_bp512r1_point); +criterion_main!(benches); diff --git a/crypto/ec/benches/bp512r1_scalar_benches.rs b/crypto/ec/benches/bp512r1_scalar_benches.rs new file mode 100644 index 000000000..f57c9fa0d --- /dev/null +++ b/crypto/ec/benches/bp512r1_scalar_benches.rs @@ -0,0 +1,72 @@ +//! Benchmarks for brainpoolP512r1's scalar side: the scalar field (arithmetic mod the +//! curve order `n`, used for `k^-1` and `s = k^-1(e + rd)` in signing) and the two scalar +//! multipliers built on it -- [`comb_multiply_base_point`], the constant-time fixed-base `[k]G` +//! that dominates key generation and signing, and [`shamir_multiply`], the variable-time `[u]G + +//! [v]Q` that dominates verification. These are the crate's most expensive routines; the field and +//! point benches next to this file measure only their building blocks. + +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::bp512r1::Bp512r1FieldElement; +use bouncycastle_ec::bp512r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp512r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp512r1_point::Bp512r1JacobianPoint; +use bouncycastle_ec::bp512r1_scalar::{Bp512r1PublicScalar, Bp512r1Scalar, Bp512r1ScalarField}; +use bouncycastle_ec::bp512r1_wnaf::shamir_multiply; + +const A_LIMBS: [u64; 8] = [ + 0x9f8573c9f25dc994, 0x9115361f42389a31, 0xfc83ab74842a0944, 0x285f078965f5a299, + 0x3d7c9ec7081ab44e, 0xbd8ec9a1f80385ed, 0x17857e0831e5554e, 0x10f26b82fe915329, +]; +const B_LIMBS: [u64; 8] = [ + 0x790d02ba68e2b293, 0x952cb98dca28e0ce, 0x1699fd1dd3e61f5f, 0x174ea8e941ac9159, + 0xc59a61378b3dda8e, 0x083d8f37af9e4e0d, 0xd253304398079d69, 0x11e815b91f17e93f, +]; + +fn bench_bp512r1_scalar(c: &mut Criterion) { + let x = Bp512r1ScalarField::from_limbs(A_LIMBS); + let y = Bp512r1ScalarField::from_limbs(B_LIMBS); + + let mut group = c.benchmark_group("bp512r1_scalar"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(x).add(black_box(&y)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(x).mul(black_box(&y)))) + }); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(x).invert()))); + // Variable-time inverse on the public scalar type (verification's `s^-1`). + let x_public = Bp512r1PublicScalar::from_limbs(A_LIMBS); + group.bench_function("invert_vartime", |bencher| { + bencher.iter(|| black_box(black_box(x_public).invert_vartime())) + }); + group.finish(); +} + +fn bench_bp512r1_scalar_mul(c: &mut Criterion) { + let k = Bp512r1Scalar::from_limbs(A_LIMBS); + let u = Bp512r1PublicScalar::from_limbs(A_LIMBS); + let v = Bp512r1PublicScalar::from_limbs(B_LIMBS); + // A stand-in for a signer's public key: any point on the curve will do for timing, and `[2]G` + // is reachable without a scalar multiplication of its own. + let g = Bp512r1JacobianPoint::from_affine( + Bp512r1FieldElement::from_limbs(G_X_LIMBS), + Bp512r1FieldElement::from_limbs(G_Y_LIMBS), + ); + let q = g.double(); + + let mut group = c.benchmark_group("bp512r1_scalar_mul"); + // Constant-time fixed-base [k]G (signing, key generation). + group.bench_function("comb_base_point", |bencher| { + bencher.iter(|| black_box(comb_multiply_base_point(black_box(&k)))) + }); + // Variable-time [u]G + [v]Q via Shamir's trick (verification). + group.bench_function("shamir", |bencher| { + bencher.iter(|| shamir_multiply(black_box(&u), black_box(&v), black_box(&q))) + }); + group.finish(); +} + +criterion_group!(benches, bench_bp512r1_scalar, bench_bp512r1_scalar_mul); +criterion_main!(benches); diff --git a/crypto/ec/benches/p256_field_benches.rs b/crypto/ec/benches/p256_field_benches.rs new file mode 100644 index 000000000..424cbcd4e --- /dev/null +++ b/crypto/ec/benches/p256_field_benches.rs @@ -0,0 +1,27 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::p256::P256FieldElement; + +fn bench_p256_field(c: &mut Criterion) { + let a = P256FieldElement::from_limbs([ + 0x1111111111111111, 0x2222222222222222, 0x3333333333333333, 0x0444444444444444, + ]); + let b = P256FieldElement::from_limbs([ + 0x5555555555555555, 0x6666666666666666, 0x7777777777777777, 0x0888888888888888, + ]); + + let mut group = c.benchmark_group("p256_field"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(a).add(black_box(&b)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(a).mul(black_box(&b)))) + }); + group.bench_function("square", |bencher| bencher.iter(|| black_box(black_box(a).square()))); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(a).invert()))); + group.finish(); +} + +criterion_group!(benches, bench_p256_field); +criterion_main!(benches); diff --git a/crypto/ec/benches/p256_point_benches.rs b/crypto/ec/benches/p256_point_benches.rs new file mode 100644 index 000000000..4034eba43 --- /dev/null +++ b/crypto/ec/benches/p256_point_benches.rs @@ -0,0 +1,42 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::p256::P256FieldElement; +use bouncycastle_ec::p256_mul::multiply_point; +use bouncycastle_ec::p256_point::P256JacobianPoint; +use bouncycastle_ec::p256_scalar::P256Scalar; + +fn bench_p256_point(c: &mut Criterion) { + // G, the P-256 base point (SP 800-186 §3.2.1.3). + let g = P256JacobianPoint::from_affine( + P256FieldElement::from_limbs([ + 0xf4a13945d898c296, 0x77037d812deb33a0, 0xf8bce6e563a440f2, 0x6b17d1f2e12c4247, + ]), + P256FieldElement::from_limbs([ + 0xcbb6406837bf51f5, 0x2bce33576b315ece, 0x8ee7eb4a7c0f9e16, 0x4fe342e2fe1a7f9b, + ]), + ); + let two_g = g.double(); + + let mut group = c.benchmark_group("p256_point"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(&g).add(black_box(&two_g)))) + }); + group.bench_function("double", |bencher| bencher.iter(|| black_box(black_box(&g).double()))); + group.bench_function("to_affine", |bencher| { + bencher.iter(|| black_box(black_box(&two_g).to_affine())) + }); + // A full-width scalar: the multiplier's cost does not depend on its value. + let mut k_limbs = [0u64; 4]; + for (i, limb) in k_limbs.iter_mut().enumerate() { + *limb = 0x9E37_79B9_7F4A_7C15 ^ (i as u64); + } + let k = P256Scalar::from_limbs(k_limbs); + group.bench_function("multiply_point", |bencher| { + bencher.iter(|| black_box(multiply_point(black_box(&k), black_box(&two_g)))) + }); + group.finish(); +} + +criterion_group!(benches, bench_p256_point); +criterion_main!(benches); diff --git a/crypto/ec/benches/p256_scalar_benches.rs b/crypto/ec/benches/p256_scalar_benches.rs new file mode 100644 index 000000000..773fce41a --- /dev/null +++ b/crypto/ec/benches/p256_scalar_benches.rs @@ -0,0 +1,68 @@ +//! Benchmarks for P-256's scalar side: the scalar field (arithmetic mod the curve order `n`, +//! used for `k^-1` and `s = k^-1(e + rd)` in signing) and the two scalar multipliers built on it -- +//! [`comb_multiply_base_point`], the constant-time fixed-base `[k]G` that dominates key generation +//! and signing, and [`shamir_multiply`], the variable-time `[u]G + [v]Q` that dominates +//! verification. These are the crate's most expensive routines; the field and point benches next to +//! this file measure only their building blocks. + +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::p256::P256FieldElement; +use bouncycastle_ec::p256_comb::comb_multiply_base_point; +use bouncycastle_ec::p256_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p256_point::P256JacobianPoint; +use bouncycastle_ec::p256_scalar::{P256PublicScalar, P256Scalar, P256ScalarField}; +use bouncycastle_ec::p256_wnaf::shamir_multiply; + +const A_LIMBS: [u64; 4] = + [0x9f8573c9f25dc994, 0x9115361f42389a31, 0xfc83ab74842a0944, 0x285f078965f5a299]; +const B_LIMBS: [u64; 4] = + [0x790d02ba68e2b293, 0x952cb98dca28e0ce, 0x1699fd1dd3e61f5f, 0x174ea8e941ac9159]; + +fn bench_p256_scalar(c: &mut Criterion) { + let x = P256ScalarField::from_limbs(A_LIMBS); + let y = P256ScalarField::from_limbs(B_LIMBS); + + let mut group = c.benchmark_group("p256_scalar"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(x).add(black_box(&y)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(x).mul(black_box(&y)))) + }); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(x).invert()))); + // Variable-time inverse on the public scalar type (verification's `s^-1`). + let x_public = P256PublicScalar::from_limbs(A_LIMBS); + group.bench_function("invert_vartime", |bencher| { + bencher.iter(|| black_box(black_box(x_public).invert_vartime())) + }); + group.finish(); +} + +fn bench_p256_scalar_mul(c: &mut Criterion) { + let k = P256Scalar::from_limbs(A_LIMBS); + let u = P256PublicScalar::from_limbs(A_LIMBS); + let v = P256PublicScalar::from_limbs(B_LIMBS); + // A stand-in for a signer's public key: any point on the curve will do for timing, and `[2]G` + // is reachable without a scalar multiplication of its own. + let g = P256JacobianPoint::from_affine( + P256FieldElement::from_limbs(G_X_LIMBS), + P256FieldElement::from_limbs(G_Y_LIMBS), + ); + let q = g.double(); + + let mut group = c.benchmark_group("p256_scalar_mul"); + // Constant-time fixed-base [k]G (signing, key generation). + group.bench_function("comb_base_point", |bencher| { + bencher.iter(|| black_box(comb_multiply_base_point(black_box(&k)))) + }); + // Variable-time [u]G + [v]Q via Shamir's trick (verification). + group.bench_function("shamir", |bencher| { + bencher.iter(|| shamir_multiply(black_box(&u), black_box(&v), black_box(&q))) + }); + group.finish(); +} + +criterion_group!(benches, bench_p256_scalar, bench_p256_scalar_mul); +criterion_main!(benches); diff --git a/crypto/ec/benches/p256k1_field_benches.rs b/crypto/ec/benches/p256k1_field_benches.rs new file mode 100644 index 000000000..f45a690bb --- /dev/null +++ b/crypto/ec/benches/p256k1_field_benches.rs @@ -0,0 +1,24 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::p256k1::P256K1FieldElement; +use bouncycastle_ec::p256k1_domain::{G_X_LIMBS, G_Y_LIMBS}; + +fn bench_p256k1_field(c: &mut Criterion) { + let a = P256K1FieldElement::from_limbs(G_X_LIMBS); + let b = P256K1FieldElement::from_limbs(G_Y_LIMBS); + + let mut group = c.benchmark_group("p256k1_field"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(a).add(black_box(&b)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(a).mul(black_box(&b)))) + }); + group.bench_function("square", |bencher| bencher.iter(|| black_box(black_box(a).square()))); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(a).invert()))); + group.finish(); +} + +criterion_group!(benches, bench_p256k1_field); +criterion_main!(benches); diff --git a/crypto/ec/benches/p256k1_point_benches.rs b/crypto/ec/benches/p256k1_point_benches.rs new file mode 100644 index 000000000..18fe1944a --- /dev/null +++ b/crypto/ec/benches/p256k1_point_benches.rs @@ -0,0 +1,38 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::p256k1::P256K1FieldElement; +use bouncycastle_ec::p256k1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p256k1_mul::multiply_point; +use bouncycastle_ec::p256k1_point::P256K1JacobianPoint; +use bouncycastle_ec::p256k1_scalar::P256K1Scalar; + +fn bench_p256k1_point(c: &mut Criterion) { + let g = P256K1JacobianPoint::from_affine( + P256K1FieldElement::from_limbs(G_X_LIMBS), + P256K1FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + + let mut group = c.benchmark_group("p256k1_point"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(&g).add(black_box(&two_g)))) + }); + group.bench_function("double", |bencher| bencher.iter(|| black_box(black_box(&g).double()))); + group.bench_function("to_affine", |bencher| { + bencher.iter(|| black_box(black_box(&two_g).to_affine())) + }); + // A full-width scalar: the multiplier's cost does not depend on its value. + let mut k_limbs = [0u64; 4]; + for (i, limb) in k_limbs.iter_mut().enumerate() { + *limb = 0x9E37_79B9_7F4A_7C15 ^ (i as u64); + } + let k = P256K1Scalar::from_limbs(k_limbs); + group.bench_function("multiply_point", |bencher| { + bencher.iter(|| black_box(multiply_point(black_box(&k), black_box(&two_g)))) + }); + group.finish(); +} + +criterion_group!(benches, bench_p256k1_point); +criterion_main!(benches); diff --git a/crypto/ec/benches/p256k1_scalar_benches.rs b/crypto/ec/benches/p256k1_scalar_benches.rs new file mode 100644 index 000000000..1cf2ce67d --- /dev/null +++ b/crypto/ec/benches/p256k1_scalar_benches.rs @@ -0,0 +1,68 @@ +//! Benchmarks for secp256k1's scalar side: the scalar field (arithmetic mod the curve order `n`, +//! used for `k^-1` and `s = k^-1(e + rd)` in signing) and the two scalar multipliers built on it -- +//! [`comb_multiply_base_point`], the constant-time fixed-base `[k]G` that dominates key generation +//! and signing, and [`shamir_multiply`], the variable-time `[u]G + [v]Q` that dominates +//! verification. These are the crate's most expensive routines; the field and point benches next to +//! this file measure only their building blocks. + +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::p256k1::P256K1FieldElement; +use bouncycastle_ec::p256k1_comb::comb_multiply_base_point; +use bouncycastle_ec::p256k1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p256k1_point::P256K1JacobianPoint; +use bouncycastle_ec::p256k1_scalar::{P256K1PublicScalar, P256K1Scalar, P256K1ScalarField}; +use bouncycastle_ec::p256k1_wnaf::shamir_multiply; + +const A_LIMBS: [u64; 4] = + [0x9f8573c9f25dc994, 0x9115361f42389a31, 0xfc83ab74842a0944, 0x285f078965f5a299]; +const B_LIMBS: [u64; 4] = + [0x790d02ba68e2b293, 0x952cb98dca28e0ce, 0x1699fd1dd3e61f5f, 0x174ea8e941ac9159]; + +fn bench_p256k1_scalar(c: &mut Criterion) { + let x = P256K1ScalarField::from_limbs(A_LIMBS); + let y = P256K1ScalarField::from_limbs(B_LIMBS); + + let mut group = c.benchmark_group("p256k1_scalar"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(x).add(black_box(&y)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(x).mul(black_box(&y)))) + }); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(x).invert()))); + // Variable-time inverse on the public scalar type (verification's `s^-1`). + let x_public = P256K1PublicScalar::from_limbs(A_LIMBS); + group.bench_function("invert_vartime", |bencher| { + bencher.iter(|| black_box(black_box(x_public).invert_vartime())) + }); + group.finish(); +} + +fn bench_p256k1_scalar_mul(c: &mut Criterion) { + let k = P256K1Scalar::from_limbs(A_LIMBS); + let u = P256K1PublicScalar::from_limbs(A_LIMBS); + let v = P256K1PublicScalar::from_limbs(B_LIMBS); + // A stand-in for a signer's public key: any point on the curve will do for timing, and `[2]G` + // is reachable without a scalar multiplication of its own. + let g = P256K1JacobianPoint::from_affine( + P256K1FieldElement::from_limbs(G_X_LIMBS), + P256K1FieldElement::from_limbs(G_Y_LIMBS), + ); + let q = g.double(); + + let mut group = c.benchmark_group("p256k1_scalar_mul"); + // Constant-time fixed-base [k]G (signing, key generation). + group.bench_function("comb_base_point", |bencher| { + bencher.iter(|| black_box(comb_multiply_base_point(black_box(&k)))) + }); + // Variable-time [u]G + [v]Q via Shamir's trick (verification). + group.bench_function("shamir", |bencher| { + bencher.iter(|| shamir_multiply(black_box(&u), black_box(&v), black_box(&q))) + }); + group.finish(); +} + +criterion_group!(benches, bench_p256k1_scalar, bench_p256k1_scalar_mul); +criterion_main!(benches); diff --git a/crypto/ec/benches/p384_field_benches.rs b/crypto/ec/benches/p384_field_benches.rs new file mode 100644 index 000000000..6d72e8c40 --- /dev/null +++ b/crypto/ec/benches/p384_field_benches.rs @@ -0,0 +1,24 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::p384::P384FieldElement; +use bouncycastle_ec::p384_domain::{G_X_LIMBS, G_Y_LIMBS}; + +fn bench_p384_field(c: &mut Criterion) { + let a = P384FieldElement::from_limbs(G_X_LIMBS); + let b = P384FieldElement::from_limbs(G_Y_LIMBS); + + let mut group = c.benchmark_group("p384_field"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(a).add(black_box(&b)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(a).mul(black_box(&b)))) + }); + group.bench_function("square", |bencher| bencher.iter(|| black_box(black_box(a).square()))); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(a).invert()))); + group.finish(); +} + +criterion_group!(benches, bench_p384_field); +criterion_main!(benches); diff --git a/crypto/ec/benches/p384_point_benches.rs b/crypto/ec/benches/p384_point_benches.rs new file mode 100644 index 000000000..b648d1262 --- /dev/null +++ b/crypto/ec/benches/p384_point_benches.rs @@ -0,0 +1,38 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::p384::P384FieldElement; +use bouncycastle_ec::p384_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p384_mul::multiply_point; +use bouncycastle_ec::p384_point::P384JacobianPoint; +use bouncycastle_ec::p384_scalar::P384Scalar; + +fn bench_p384_point(c: &mut Criterion) { + let g = P384JacobianPoint::from_affine( + P384FieldElement::from_limbs(G_X_LIMBS), + P384FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + + let mut group = c.benchmark_group("p384_point"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(&g).add(black_box(&two_g)))) + }); + group.bench_function("double", |bencher| bencher.iter(|| black_box(black_box(&g).double()))); + group.bench_function("to_affine", |bencher| { + bencher.iter(|| black_box(black_box(&two_g).to_affine())) + }); + // A full-width scalar: the multiplier's cost does not depend on its value. + let mut k_limbs = [0u64; 6]; + for (i, limb) in k_limbs.iter_mut().enumerate() { + *limb = 0x9E37_79B9_7F4A_7C15 ^ (i as u64); + } + let k = P384Scalar::from_limbs(k_limbs); + group.bench_function("multiply_point", |bencher| { + bencher.iter(|| black_box(multiply_point(black_box(&k), black_box(&two_g)))) + }); + group.finish(); +} + +criterion_group!(benches, bench_p384_point); +criterion_main!(benches); diff --git a/crypto/ec/benches/p384_scalar_benches.rs b/crypto/ec/benches/p384_scalar_benches.rs new file mode 100644 index 000000000..3d4f7252d --- /dev/null +++ b/crypto/ec/benches/p384_scalar_benches.rs @@ -0,0 +1,72 @@ +//! Benchmarks for P-384's scalar side: the scalar field (arithmetic mod the curve order `n`, +//! used for `k^-1` and `s = k^-1(e + rd)` in signing) and the two scalar multipliers built on it -- +//! [`comb_multiply_base_point`], the constant-time fixed-base `[k]G` that dominates key generation +//! and signing, and [`shamir_multiply`], the variable-time `[u]G + [v]Q` that dominates +//! verification. These are the crate's most expensive routines; the field and point benches next to +//! this file measure only their building blocks. + +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::p384::P384FieldElement; +use bouncycastle_ec::p384_comb::comb_multiply_base_point; +use bouncycastle_ec::p384_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p384_point::P384JacobianPoint; +use bouncycastle_ec::p384_scalar::{P384PublicScalar, P384Scalar, P384ScalarField}; +use bouncycastle_ec::p384_wnaf::shamir_multiply; + +const A_LIMBS: [u64; 6] = [ + 0x9f8573c9f25dc994, 0x9115361f42389a31, 0xfc83ab74842a0944, 0x285f078965f5a299, + 0x3d7c9ec7081ab44e, 0xbd8ec9a1f80385ed, +]; +const B_LIMBS: [u64; 6] = [ + 0x790d02ba68e2b293, 0x952cb98dca28e0ce, 0x1699fd1dd3e61f5f, 0x174ea8e941ac9159, + 0xc59a61378b3dda8e, 0x083d8f37af9e4e0d, +]; + +fn bench_p384_scalar(c: &mut Criterion) { + let x = P384ScalarField::from_limbs(A_LIMBS); + let y = P384ScalarField::from_limbs(B_LIMBS); + + let mut group = c.benchmark_group("p384_scalar"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(x).add(black_box(&y)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(x).mul(black_box(&y)))) + }); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(x).invert()))); + // Variable-time inverse on the public scalar type (verification's `s^-1`). + let x_public = P384PublicScalar::from_limbs(A_LIMBS); + group.bench_function("invert_vartime", |bencher| { + bencher.iter(|| black_box(black_box(x_public).invert_vartime())) + }); + group.finish(); +} + +fn bench_p384_scalar_mul(c: &mut Criterion) { + let k = P384Scalar::from_limbs(A_LIMBS); + let u = P384PublicScalar::from_limbs(A_LIMBS); + let v = P384PublicScalar::from_limbs(B_LIMBS); + // A stand-in for a signer's public key: any point on the curve will do for timing, and `[2]G` + // is reachable without a scalar multiplication of its own. + let g = P384JacobianPoint::from_affine( + P384FieldElement::from_limbs(G_X_LIMBS), + P384FieldElement::from_limbs(G_Y_LIMBS), + ); + let q = g.double(); + + let mut group = c.benchmark_group("p384_scalar_mul"); + // Constant-time fixed-base [k]G (signing, key generation). + group.bench_function("comb_base_point", |bencher| { + bencher.iter(|| black_box(comb_multiply_base_point(black_box(&k)))) + }); + // Variable-time [u]G + [v]Q via Shamir's trick (verification). + group.bench_function("shamir", |bencher| { + bencher.iter(|| shamir_multiply(black_box(&u), black_box(&v), black_box(&q))) + }); + group.finish(); +} + +criterion_group!(benches, bench_p384_scalar, bench_p384_scalar_mul); +criterion_main!(benches); diff --git a/crypto/ec/benches/p521_field_benches.rs b/crypto/ec/benches/p521_field_benches.rs new file mode 100644 index 000000000..d1a5638de --- /dev/null +++ b/crypto/ec/benches/p521_field_benches.rs @@ -0,0 +1,24 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::p521::P521FieldElement; +use bouncycastle_ec::p521_domain::{G_X_LIMBS, G_Y_LIMBS}; + +fn bench_p521_field(c: &mut Criterion) { + let a = P521FieldElement::from_limbs(G_X_LIMBS); + let b = P521FieldElement::from_limbs(G_Y_LIMBS); + + let mut group = c.benchmark_group("p521_field"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(a).add(black_box(&b)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(a).mul(black_box(&b)))) + }); + group.bench_function("square", |bencher| bencher.iter(|| black_box(black_box(a).square()))); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(a).invert()))); + group.finish(); +} + +criterion_group!(benches, bench_p521_field); +criterion_main!(benches); diff --git a/crypto/ec/benches/p521_point_benches.rs b/crypto/ec/benches/p521_point_benches.rs new file mode 100644 index 000000000..db1b84f52 --- /dev/null +++ b/crypto/ec/benches/p521_point_benches.rs @@ -0,0 +1,38 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::p521::P521FieldElement; +use bouncycastle_ec::p521_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p521_mul::multiply_point; +use bouncycastle_ec::p521_point::P521JacobianPoint; +use bouncycastle_ec::p521_scalar::P521Scalar; + +fn bench_p521_point(c: &mut Criterion) { + let g = P521JacobianPoint::from_affine( + P521FieldElement::from_limbs(G_X_LIMBS), + P521FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + + let mut group = c.benchmark_group("p521_point"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(&g).add(black_box(&two_g)))) + }); + group.bench_function("double", |bencher| bencher.iter(|| black_box(black_box(&g).double()))); + group.bench_function("to_affine", |bencher| { + bencher.iter(|| black_box(black_box(&two_g).to_affine())) + }); + // A full-width scalar: the multiplier's cost does not depend on its value. + let mut k_limbs = [0u64; 9]; + for (i, limb) in k_limbs.iter_mut().enumerate() { + *limb = 0x9E37_79B9_7F4A_7C15 ^ (i as u64); + } + let k = P521Scalar::from_limbs(k_limbs); + group.bench_function("multiply_point", |bencher| { + bencher.iter(|| black_box(multiply_point(black_box(&k), black_box(&two_g)))) + }); + group.finish(); +} + +criterion_group!(benches, bench_p521_point); +criterion_main!(benches); diff --git a/crypto/ec/benches/p521_scalar_benches.rs b/crypto/ec/benches/p521_scalar_benches.rs new file mode 100644 index 000000000..d0b42b441 --- /dev/null +++ b/crypto/ec/benches/p521_scalar_benches.rs @@ -0,0 +1,74 @@ +//! Benchmarks for P-521's scalar side: the scalar field (arithmetic mod the curve order `n`, +//! used for `k^-1` and `s = k^-1(e + rd)` in signing) and the two scalar multipliers built on it -- +//! [`comb_multiply_base_point`], the constant-time fixed-base `[k]G` that dominates key generation +//! and signing, and [`shamir_multiply`], the variable-time `[u]G + [v]Q` that dominates +//! verification. These are the crate's most expensive routines; the field and point benches next to +//! this file measure only their building blocks. + +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::p521::P521FieldElement; +use bouncycastle_ec::p521_comb::comb_multiply_base_point; +use bouncycastle_ec::p521_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p521_point::P521JacobianPoint; +use bouncycastle_ec::p521_scalar::{P521PublicScalar, P521Scalar, P521ScalarField}; +use bouncycastle_ec::p521_wnaf::shamir_multiply; + +const A_LIMBS: [u64; 9] = [ + 0x9f8573c9f25dc994, 0x9115361f42389a31, 0xfc83ab74842a0944, 0x285f078965f5a299, + 0x3d7c9ec7081ab44e, 0xbd8ec9a1f80385ed, 0x17857e0831e5554e, 0x10f26b82fe915329, + 0x0000000000000101, +]; +const B_LIMBS: [u64; 9] = [ + 0x790d02ba68e2b293, 0x952cb98dca28e0ce, 0x1699fd1dd3e61f5f, 0x174ea8e941ac9159, + 0xc59a61378b3dda8e, 0x083d8f37af9e4e0d, 0xd253304398079d69, 0x11e815b91f17e93f, + 0x00000000000000b7, +]; + +fn bench_p521_scalar(c: &mut Criterion) { + let x = P521ScalarField::from_limbs(A_LIMBS); + let y = P521ScalarField::from_limbs(B_LIMBS); + + let mut group = c.benchmark_group("p521_scalar"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(x).add(black_box(&y)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(x).mul(black_box(&y)))) + }); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(x).invert()))); + // Variable-time inverse on the public scalar type (verification's `s^-1`). + let x_public = P521PublicScalar::from_limbs(A_LIMBS); + group.bench_function("invert_vartime", |bencher| { + bencher.iter(|| black_box(black_box(x_public).invert_vartime())) + }); + group.finish(); +} + +fn bench_p521_scalar_mul(c: &mut Criterion) { + let k = P521Scalar::from_limbs(A_LIMBS); + let u = P521PublicScalar::from_limbs(A_LIMBS); + let v = P521PublicScalar::from_limbs(B_LIMBS); + // A stand-in for a signer's public key: any point on the curve will do for timing, and `[2]G` + // is reachable without a scalar multiplication of its own. + let g = P521JacobianPoint::from_affine( + P521FieldElement::from_limbs(G_X_LIMBS), + P521FieldElement::from_limbs(G_Y_LIMBS), + ); + let q = g.double(); + + let mut group = c.benchmark_group("p521_scalar_mul"); + // Constant-time fixed-base [k]G (signing, key generation). + group.bench_function("comb_base_point", |bencher| { + bencher.iter(|| black_box(comb_multiply_base_point(black_box(&k)))) + }); + // Variable-time [u]G + [v]Q via Shamir's trick (verification). + group.bench_function("shamir", |bencher| { + bencher.iter(|| shamir_multiply(black_box(&u), black_box(&v), black_box(&q))) + }); + group.finish(); +} + +criterion_group!(benches, bench_p521_scalar, bench_p521_scalar_mul); +criterion_main!(benches); diff --git a/crypto/ec/benches/sm2_field_benches.rs b/crypto/ec/benches/sm2_field_benches.rs new file mode 100644 index 000000000..f55ffb72c --- /dev/null +++ b/crypto/ec/benches/sm2_field_benches.rs @@ -0,0 +1,24 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::sm2::Sm2FieldElement; +use bouncycastle_ec::sm2_domain::{G_X_LIMBS, G_Y_LIMBS}; + +fn bench_sm2_field(c: &mut Criterion) { + let a = Sm2FieldElement::from_limbs(G_X_LIMBS); + let b = Sm2FieldElement::from_limbs(G_Y_LIMBS); + + let mut group = c.benchmark_group("sm2_field"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(a).add(black_box(&b)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(a).mul(black_box(&b)))) + }); + group.bench_function("square", |bencher| bencher.iter(|| black_box(black_box(a).square()))); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(a).invert()))); + group.finish(); +} + +criterion_group!(benches, bench_sm2_field); +criterion_main!(benches); diff --git a/crypto/ec/benches/sm2_point_benches.rs b/crypto/ec/benches/sm2_point_benches.rs new file mode 100644 index 000000000..f2764c8d6 --- /dev/null +++ b/crypto/ec/benches/sm2_point_benches.rs @@ -0,0 +1,27 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::sm2::Sm2FieldElement; +use bouncycastle_ec::sm2_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::sm2_point::Sm2JacobianPoint; + +fn bench_sm2_point(c: &mut Criterion) { + let g = Sm2JacobianPoint::from_affine( + Sm2FieldElement::from_limbs(G_X_LIMBS), + Sm2FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + + let mut group = c.benchmark_group("sm2_point"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(&g).add(black_box(&two_g)))) + }); + group.bench_function("double", |bencher| bencher.iter(|| black_box(black_box(&g).double()))); + group.bench_function("to_affine", |bencher| { + bencher.iter(|| black_box(black_box(&two_g).to_affine())) + }); + group.finish(); +} + +criterion_group!(benches, bench_sm2_point); +criterion_main!(benches); diff --git a/crypto/ec/benches/sm2_scalar_benches.rs b/crypto/ec/benches/sm2_scalar_benches.rs new file mode 100644 index 000000000..a53ec0414 --- /dev/null +++ b/crypto/ec/benches/sm2_scalar_benches.rs @@ -0,0 +1,63 @@ +//! Benchmarks for SM2's scalar side: the scalar field (arithmetic mod the curve order `n`, +//! used for `k^-1` and `s = k^-1(e + rd)` in signing) and the two scalar multipliers built on it -- +//! [`comb_multiply_base_point`], the constant-time fixed-base `[k]G` that dominates key generation +//! and signing, and [`shamir_multiply`], the variable-time `[u]G + [v]Q` that dominates +//! verification. These are the crate's most expensive routines; the field and point benches next to +//! this file measure only their building blocks. + +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ec::sm2::Sm2FieldElement; +use bouncycastle_ec::sm2_comb::comb_multiply_base_point; +use bouncycastle_ec::sm2_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::sm2_point::Sm2JacobianPoint; +use bouncycastle_ec::sm2_scalar::{Sm2PublicScalar, Sm2Scalar, Sm2ScalarField}; +use bouncycastle_ec::sm2_wnaf::shamir_multiply; + +const A_LIMBS: [u64; 4] = + [0x9f8573c9f25dc994, 0x9115361f42389a31, 0xfc83ab74842a0944, 0x285f078965f5a299]; +const B_LIMBS: [u64; 4] = + [0x790d02ba68e2b293, 0x952cb98dca28e0ce, 0x1699fd1dd3e61f5f, 0x174ea8e941ac9159]; + +fn bench_sm2_scalar(c: &mut Criterion) { + let x = Sm2ScalarField::from_limbs(A_LIMBS); + let y = Sm2ScalarField::from_limbs(B_LIMBS); + + let mut group = c.benchmark_group("sm2_scalar"); + group.bench_function("add", |bencher| { + bencher.iter(|| black_box(black_box(x).add(black_box(&y)))) + }); + group.bench_function("mul", |bencher| { + bencher.iter(|| black_box(black_box(x).mul(black_box(&y)))) + }); + group.bench_function("invert", |bencher| bencher.iter(|| black_box(black_box(x).invert()))); + group.finish(); +} + +fn bench_sm2_scalar_mul(c: &mut Criterion) { + let k = Sm2Scalar::from_limbs(A_LIMBS); + let u = Sm2PublicScalar::from_limbs(A_LIMBS); + let v = Sm2PublicScalar::from_limbs(B_LIMBS); + // A stand-in for a signer's public key: any point on the curve will do for timing, and `[2]G` + // is reachable without a scalar multiplication of its own. + let g = Sm2JacobianPoint::from_affine( + Sm2FieldElement::from_limbs(G_X_LIMBS), + Sm2FieldElement::from_limbs(G_Y_LIMBS), + ); + let q = g.double(); + + let mut group = c.benchmark_group("sm2_scalar_mul"); + // Constant-time fixed-base [k]G (signing, key generation). + group.bench_function("comb_base_point", |bencher| { + bencher.iter(|| black_box(comb_multiply_base_point(black_box(&k)))) + }); + // Variable-time [u]G + [v]Q via Shamir's trick (verification). + group.bench_function("shamir", |bencher| { + bencher.iter(|| shamir_multiply(black_box(&u), black_box(&v), black_box(&q))) + }); + group.finish(); +} + +criterion_group!(benches, bench_sm2_scalar, bench_sm2_scalar_mul); +criterion_main!(benches); diff --git a/crypto/ec/examples/generate_p256_comb_table.rs b/crypto/ec/examples/generate_p256_comb_table.rs new file mode 100644 index 000000000..5c09f8b63 --- /dev/null +++ b/crypto/ec/examples/generate_p256_comb_table.rs @@ -0,0 +1,77 @@ +//! Regenerates `src/p256_comb_table.rs`'s `COMB_TABLE_X`/`COMB_TABLE_Y` from scratch and prints +//! them as Rust source, using only the crate's public point arithmetic (this binary, like every +//! `examples/` target, only sees the crate's public API -- `p256_comb_table` is `pub(crate)`, so +//! the actual "does the checked-in table still match" check lives in that module's own unit test, +//! which can see it; this binary is for producing the values to paste in when the construction +//! ever changes). +//! +//! ```text +//! cargo run -p bouncycastle-ec --example generate_p256_comb_table +//! ``` + +use bouncycastle_ec::p256::P256FieldElement; +use bouncycastle_ec::p256_point::P256JacobianPoint; + +const WIDTH: usize = 6; +const BITS: usize = 256; +const TABLE_SIZE: usize = 1 << WIDTH; + +// SP 800-186 (Feb 2023) §3.2.1.3, the P-256 base point G. +const G_X: [u64; 4] = + [0xf4a13945d898c296, 0x77037d812deb33a0, 0xf8bce6e563a440f2, 0x6b17d1f2e12c4247]; +const G_Y: [u64; 4] = + [0xcbb6406837bf51f5, 0x2bce33576b315ece, 0x8ee7eb4a7c0f9e16, 0x4fe342e2fe1a7f9b]; + +fn print_limbs(limbs: [u64; 4]) { + println!( + " [0x{:016x}, 0x{:016x}, 0x{:016x}, 0x{:016x}],", + limbs[0], limbs[1], limbs[2], limbs[3] + ); +} + +fn main() { + let d = BITS.div_ceil(WIDTH); + + let g = P256JacobianPoint::from_affine( + P256FieldElement::from_limbs(G_X), + P256FieldElement::from_limbs(G_Y), + ); + + let mut pow2 = [g; WIDTH]; + for i in 1..WIDTH { + let mut p = pow2[i - 1]; + for _ in 0..d { + p = p.double(); + } + pow2[i] = p; + } + + let mut table = [P256JacobianPoint::INFINITY; TABLE_SIZE]; + for bit in (0..WIDTH).rev() { + let step = 1usize << bit; + let pw = pow2[bit]; + let mut i = step; + while i < TABLE_SIZE { + table[i] = table[i - step].add(&pw); + i += 2 * step; + } + } + + println!("pub(crate) const COMB_TABLE_X: [[u64; 4]; {TABLE_SIZE}] = ["); + for point in &table { + match point.to_affine() { + None => print_limbs([0, 0, 0, 0]), + Some((x, _)) => print_limbs(x.to_limbs()), + } + } + println!("];"); + println!(); + println!("pub(crate) const COMB_TABLE_Y: [[u64; 4]; {TABLE_SIZE}] = ["); + for point in &table { + match point.to_affine() { + None => print_limbs([0, 0, 0, 0]), + Some((_, y)) => print_limbs(y.to_limbs()), + } + } + println!("];"); +} diff --git a/crypto/ec/src/barrett.rs b/crypto/ec/src/barrett.rs new file mode 100644 index 000000000..211850d81 --- /dev/null +++ b/crypto/ec/src/barrett.rs @@ -0,0 +1,106 @@ +//! Constant-time Barrett reduction of a wide value by a modulus that has no arithmetic shortcut +//! (Menezes, van Oorschot & Vanstone, *Handbook of Applied Cryptography*, Algorithm 14.42, in the +//! limb-aligned form below), generic over the limb count `L` like [`crate::nat`]. +//! +//! The one user is FIPS 186-5 Appendix A.4.1's `x mod (n - 1)` in `bouncycastle-ecdsa` and +//! `bouncycastle-sm2`: the DRBG output `x` is 64 to 96 bits wider than `n`, and `n - 1` is even, so +//! neither the per-curve Solinas reductions nor Montgomery arithmetic applies. The previous +//! implementation reduced `x` one bit at a time -- a doubling, a conditional carry correction and +//! a conditional subtraction per bit, 320 to 592 times -- which cost 10 to 15 microseconds, more +//! than a field inversion, on every key generation and every randomised signature. Barrett's +//! method does the same job with two `L x L` multiplications and a fixed number of conditional +//! subtractions, every step of it branch-free in the value being reduced. +//! +//! # The algorithm, and its bound +//! +//! Write `k = 64 L`, and require the modulus to fill its top limb: `2^(k-1) < m < 2^k`. Precompute +//! `mu = floor(2^(2k) / m)`; since `m > 2^(k-1)`, `2^k < mu < 2^(k+1)`, so `mu = 2^k + mu_low` with +//! `mu_low < 2^k`, and [`reduce`] takes only `mu_low`. For an input `T < m * 2^k`: +//! +//! ```text +//! t_hi = floor(T / 2^k) (the top L limbs; t_hi < m) +//! q_hat = floor(t_hi * mu / 2^k) +//! = t_hi + floor(t_hi * mu_low / 2^k) (the top L limbs of an L x L product) +//! r = T - q_hat * m +//! ``` +//! +//! With `q = floor(T / m)`: `q_hat <= q`, since `t_hi <= T / 2^k` and `mu <= 2^(2k) / m`; and +//! `q_hat >= q - 3`, since `t_hi > T / 2^k - 1`, `mu > 2^(2k) / m - 1`, `T / 2^(2k) < m / 2^k < 1` +//! and `2^k / m < 2`, so `t_hi * mu / 2^k > T / m - 3`. Hence `0 <= r < 4 m`, and three conditional +//! subtractions of `m` finish the reduction. (Measured over the seven moduli this crate's users +//! pass, at most two are ever needed; the third is kept because the bound, not the measurement, +//! is what makes the code correct.) +//! +//! Verified (not checked in) in Python against arbitrary-precision `%` for each of the seven +//! `n - 1` values, over 20,000 pseudorandom inputs per modulus at every byte length up to the +//! maximum, plus the extremes `0`, `1`, `m - 1`, `m`, `m + 1`, `m * 2^k - 1` and the all-ones +//! input of maximum width. + +use crate::montgomery::widening_mul; +use crate::nat; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// Packs up to `8 * L2` big-endian bytes into `L2` little-endian limbs, zero-extended at the top. +/// Panics if `bytes` is longer than the limbs can hold: a caller passing the wrong width is a +/// programming error, not a data error. +pub fn limbs_from_be_bytes(bytes: &[u8]) -> [u64; L2] { + assert!(bytes.len() <= 8 * L2, "{} bytes do not fit in {L2} limbs", bytes.len()); + let mut limbs = [0u64; L2]; + for (i, &byte) in bytes.iter().rev().enumerate() { + limbs[i / 8] |= (byte as u64) << (8 * (i % 8)); + } + limbs +} + +/// `t mod m` for a `2L`-limb `t < m * 2^(64L)` and an `L`-limb modulus `2^(64L-1) < m < 2^(64L)`, +/// given `mu_low = floor(2^(128L) / m) - 2^(64L)`. `L2` must be `2L` and `L1` must be `L + 1` +/// (the width `r < 4m` needs before its final subtractions); both are debug-asserted, as are the +/// preconditions on `t` and `m`. Branch-free in `t`. +pub fn reduce( + t: &[u64; L2], + m: &[u64; L], + mu_low: &[u64; L], +) -> [u64; L] { + debug_assert_eq!(L2, 2 * L, "reduce's L2 must be exactly 2 * L"); + debug_assert_eq!(L1, L + 1, "reduce's L1 must be exactly L + 1"); + debug_assert!(m[L - 1] >> 63 == 1, "reduce needs a modulus with its top bit set"); + + // t_hi = floor(t / 2^k), the top L limbs; the precondition t < m * 2^k is t_hi < m. + let mut t_hi = [0u64; L]; + t_hi.copy_from_slice(&t[L..]); + debug_assert_eq!(nat::sub(&t_hi, m).1, 1, "reduce needs t < m * 2^(64L)"); + + // q_hat = t_hi + floor(t_hi * mu_low / 2^k): the top L limbs of the product, plus t_hi. The + // sum cannot carry: q_hat <= q = floor(t / m) < 2^k. + let product = widening_mul::(&t_hi, mu_low); + let mut product_hi = [0u64; L]; + product_hi.copy_from_slice(&product[L..]); + let (q_hat, carry) = nat::add(&t_hi, &product_hi); + debug_assert_eq!(carry, 0, "q_hat overflowed L limbs, contradicting q_hat <= q < 2^(64L)"); + + // r = t - q_hat * m, over 2L limbs; never negative since q_hat <= q, and < 4m since + // q_hat >= q - 3, so it fits in L + 1 limbs. + let q_hat_m = widening_mul::(&q_hat, m); + let (r_wide, borrow) = nat::sub(t, &q_hat_m); + debug_assert_eq!(borrow, 0, "t - q_hat * m went negative, contradicting q_hat <= q"); + debug_assert!(r_wide[L1..].iter().all(|&limb| limb == 0), "r exceeded 4m"); + let mut r = [0u64; L1]; + r.copy_from_slice(&r_wide[..L1]); + + // Three conditional subtractions bring r from [0, 4m) into [0, m). Each is a masked select, + // so the number performed does not depend on r. + let mut m_wide = [0u64; L1]; + m_wide[..L].copy_from_slice(m); + for _ in 0..3 { + let (diff, borrow) = nat::sub(&r, &m_wide); + let mut selected = [0u64; L1]; + ct::conditional_select(Condition::::from_lsb(borrow), &r, &diff, &mut selected); + r = selected; + } + debug_assert_eq!(r[L], 0, "r still exceeds L limbs after three subtractions"); + + let mut out = [0u64; L]; + out.copy_from_slice(&r[..L]); + out +} diff --git a/crypto/ec/src/bp256r1.rs b/crypto/ec/src/bp256r1.rs new file mode 100644 index 000000000..c227ff13d --- /dev/null +++ b/crypto/ec/src/bp256r1.rs @@ -0,0 +1,225 @@ +//! The brainpoolP256r1 base field, GF(p) (RFC 5639 §3.4). Unlike every NIST curve or secp256k1 in +//! this crate, `p` here is a "random" prime with no Solinas-style algebraic shortcut for +//! reduction (RFC 5639 §3.1's curves are generated pseudorandomly, not chosen for a special +//! bit-pattern) -- so this field, like [`crate::bp256r1_scalar`], is built on +//! [`crate::montgomery`]'s generic Montgomery multiplication instead of a per-curve fold. +//! +//! `p`'s value and the base point's coordinates were extracted directly from RFC 5639 §3.4's text +//! (programmatically, concatenating the line-wrapped hex, not retyped) and verified in Python: `G` +//! is on the curve and `n·G` is the point at infinity, before any of this was ported to Rust. + +use crate::montgomery; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// `p`, little-endian `u64` limbs (RFC 5639 §3.4). +pub const P_LIMBS: [u64; 4] = + [0x2013481d1f6e5377, 0x6e3bf623d5262028, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc]; + +/// `-p^-1 mod 2^64`, the Montgomery reduction constant. +const N_PRIME: u64 = 0xc6a75590cefd89b9; + +/// `R^2 mod p` (`R = 2^256`), used to bring a plain value into Montgomery form. +const R_SQUARED_LIMBS: [u64; 4] = + [0x8cfedf7ba6465b6c, 0x5cce4c26614d4f4d, 0xa1ecdacd6b1ac807, 0x4717aa21e5957fa8]; + +/// `R mod p`, also the Montgomery representation of `1`. +const R_MOD_P_LIMBS: [u64; 4] = + [0xdfecb7e2e091ac89, 0x91c409dc2ad9dfd7, 0xc199f56f627c728d, 0x5604a8245e115643]; + +/// `p - 2`, little-endian `u64` limbs -- the public exponent [`Bp256r1FieldElement::invert`] +/// raises its base to, per Fermat's little theorem. +const P_MINUS_2_LIMBS: [u64; 4] = + [0x2013481d1f6e5375, 0x6e3bf623d5262028, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc]; + +/// An element of the brainpoolP256r1 base field GF(p). Stored internally in Montgomery form. +#[derive(Clone, Copy, Debug)] +pub struct Bp256r1FieldElement([u64; 4]); + +impl Bp256r1FieldElement { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0]); + + /// The multiplicative identity. Its Montgomery form is `R mod p`, not the literal integer `1`. + pub const ONE: Self = Self(R_MOD_P_LIMBS); + + /// Builds a field element from little-endian `u64` limbs (an ordinary, non-Montgomery value), + /// reducing once if `limbs >= p` (valid since `limbs < 2^256 < 2p`: `p`'s top limb is + /// `0xa9fb...`, comfortably more than half of `u64::MAX`, so `2p > 2^256`), then converting to + /// Montgomery form. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + let (diff, borrow) = crate::nat::sub(&limbs, &P_LIMBS); + let mut reduced = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + let t = montgomery::widening_mul::<4, 8>(&reduced, &R_SQUARED_LIMBS); + Self(Self::finish_redc(&t)) + } + + /// Returns the canonical little-endian `u64` limbs (an ordinary, non-Montgomery value), `< p`. + pub fn to_limbs(&self) -> [u64; 4] { + let mut t = [0u64; 8]; + t[..4].copy_from_slice(&self.0); + montgomery::redc::<4, 8, 9>(&t, &P_LIMBS, N_PRIME).0 + } + + /// The limbs exactly as this type stores them: the Montgomery form `x*R mod p`, not the + /// integer `x`. Paired with [`Self::from_internal_limbs`] so the crate's point arithmetic can + /// select between the coordinates of two existing points without converting each one out of + /// and back into Montgomery form -- a REDC and a multiplication per coordinate, which is what + /// [`Self::to_limbs`] followed by [`Self::from_limbs`] costs. + pub(crate) fn internal_limbs(&self) -> [u64; 4] { + self.0 + } + + /// Wraps limbs already in this type's stored representation (see [`Self::internal_limbs`]), + /// skipping the reduction and conversion [`Self::from_limbs`] performs. Only for values that + /// came out of another element of this type, or a constant computed in that representation: + /// the masked selects in the point arithmetic, and the comb table's infinity sentinel. Not + /// exposed, since an out-of-range value would break every equality and reduction assumption + /// downstream; the range is checked in debug builds. + pub(crate) fn from_internal_limbs(limbs: [u64; 4]) -> Self { + debug_assert_eq!( + crate::nat::sub(&limbs, &P_LIMBS).1, + 1, + "from_internal_limbs given a value >= p" + ); + Self(limbs) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + crate::nat::is_zero(&self.0) + } + + /// Overwrites the element with zero, for scrubbing a secret-derived value before it goes out + /// of scope. Defence in depth rather than a guarantee: copies the compiler kept in registers + /// or spilled itself are not reached. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self + other mod p`. Operates directly on the Montgomery-form limbs: Montgomery form is + /// linear, so this needs no REDC, just the same carry-aware correction every Montgomery + /// scalar field in this crate uses -- `R_MOD_P_LIMBS` (`R mod p`), not `P_LIMBS`, since a + /// carry out of the top limb means the true sum is `sum + 2^256`, and `2^256 mod p` is + /// `R_MOD_P_LIMBS` by definition, not `p` itself. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = crate::nat::add(&self.0, &other.0); + let (sum_plus_r, _) = crate::nat::add(&sum, &R_MOD_P_LIMBS); + let (diff, borrow) = crate::nat::sub(&sum, &P_LIMBS); + let mut when_no_carry = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_r, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod p`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = crate::nat::sub(&self.0, &other.0); + let (corrected, _) = crate::nat::add(&diff, &P_LIMBS); + let mut result = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod p`. + pub fn negate(&self) -> Self { + let (diff, _) = crate::nat::sub(&P_LIMBS, &self.0); + let mut result = [0u64; 4]; + ct::conditional_select(self.is_zero(), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod p`, via Montgomery multiplication. + pub fn mul(&self, other: &Self) -> Self { + let t = montgomery::widening_mul::<4, 8>(&self.0, &other.0); + Self(Self::finish_redc(&t)) + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`montgomery::widening_square`]. Used by the exponentiation loops, not by point + /// arithmetic; see [`crate::p256::P256FieldElement::square`] for the measurement behind that + /// split. + pub fn square(&self) -> Self { + let t = montgomery::widening_square::<4, 8>(&self.0); + Self(Self::finish_redc(&t)) + } + + /// `self^-1 mod p`, or `0` if `self` is `0`. Fermat's little theorem over the public + /// exponent `p-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..4).rev() { + let limb = P_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } + + /// [`montgomery::redc`]'s `(high, extra)` result, reduced to the canonical `< p` value: if + /// `extra == 1`, `high + R_MOD_P_LIMBS` (proven to need no further reduction, the same + /// invariant every per-curve `redc` caller in this crate relies on -- verified, not checked + /// in, against 20,000 random trials plus the `(p-1)*(p-1)` worst case for this specific `p`); + /// otherwise `high`, minus `p` once more if `high >= p`. + fn finish_redc(t: &[u64; 8]) -> [u64; 4] { + let (high, extra) = montgomery::redc::<4, 8, 9>(t, &P_LIMBS, N_PRIME); + let (sum, _) = crate::nat::add(&high, &R_MOD_P_LIMBS); + let (diff, borrow) = crate::nat::sub(&high, &P_LIMBS); + let mut when_no_extra = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(borrow), + &high, + &diff, + &mut when_no_extra, + ); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(extra), + &sum, + &when_no_extra, + &mut result, + ); + result + } +} + +impl PartialEq for Bp256r1FieldElement { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. Comparing Montgomery-form limbs directly is valid: the map `x -> x*R mod p` is a + /// bijection. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..4 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for Bp256r1FieldElement {} diff --git a/crypto/ec/src/bp256r1_comb.rs b/crypto/ec/src/bp256r1_comb.rs new file mode 100644 index 000000000..e9114ffd9 --- /dev/null +++ b/crypto/ec/src/bp256r1_comb.rs @@ -0,0 +1,70 @@ +//! Fixed-base comb scalar multiplication: `[k]G` for brainpoolP256r1's base point `G`, with `k` +//! secret. Identical algorithm to [`crate::p256_comb`] -- see that module's docs for the full +//! derivation and verification methodology -- with `D * WIDTH` again not dividing `BITS` exactly +//! (`256 / 6 = 42.67`, so `D = 43`, `FULL_COMB = 258`, zero-padded), same as P-256's own case. +//! +//! Verified (not checked in) against 200 random `k` plus edge cases (`k = 1, 2, 3, n-1`), each +//! cross-checked against the standard affine group law (general `a`). + +use crate::bp256r1::Bp256r1FieldElement; +use crate::bp256r1_comb_table::{COMB_TABLE_X, COMB_TABLE_Y}; +use crate::bp256r1_point::Bp256r1JacobianPoint; +use crate::bp256r1_scalar::Bp256r1Scalar; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 6; +const BITS: usize = 256; +const D: usize = BITS.div_ceil(WIDTH); +const FULL_COMB: usize = D * WIDTH; +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]G`, branch-free in `k`. +pub fn comb_multiply_base_point(k: &Bp256r1Scalar) -> Bp256r1JacobianPoint { + let limbs = k.limbs(); + let mut r = Bp256r1JacobianPoint::INFINITY; + let top = (FULL_COMB - 1) as isize; + for i in 0..D { + let mut secret_index: usize = 0; + let mut j = top - i as isize; + while j >= 0 { + let bit = if (j as usize) < BITS { + (limbs[(j as usize) / 64] >> ((j as usize) % 64)) & 1 + } else { + 0 + }; + // `secret_index << 1`'s vacated low bit and `bit` (always `0` or `1`) are disjoint, so + // `|` and `^` agree here. + secret_index = (secret_index << 1) | (bit as usize); + j -= D as isize; + } + r = r.double(); + let (x, y, is_infinity) = table_lookup(secret_index); + r = r.add_affine(&x, &y, is_infinity); + } + r +} + +/// Looks up comb table entry `secret_index`, scanning every one of the [`TABLE_SIZE`] entries +/// under a mask. See [`crate::p256_comb::table_lookup`]'s docs. +fn table_lookup(secret_index: usize) -> (Bp256r1FieldElement, Bp256r1FieldElement, Condition) { + let mut x_limbs = [0u64; 4]; + let mut y_limbs = [0u64; 4]; + for i in 0..TABLE_SIZE { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + let mut next_x = [0u64; 4]; + ct::conditional_select(is_this_entry, &COMB_TABLE_X[i], &x_limbs, &mut next_x); + x_limbs = next_x; + let mut next_y = [0u64; 4]; + ct::conditional_select(is_this_entry, &COMB_TABLE_Y[i], &y_limbs, &mut next_y); + y_limbs = next_y; + } + // Entry 0 is the point at infinity; every other entry is affine (Z == 1). The accumulator's + // `add_affine` takes the identity as a flag rather than as a `Z == 0` coordinate. + let is_infinity = Condition::::is_equal(secret_index as u64, 0); + ( + Bp256r1FieldElement::from_limbs(x_limbs), + Bp256r1FieldElement::from_limbs(y_limbs), + is_infinity, + ) +} diff --git a/crypto/ec/src/bp256r1_comb_table.rs b/crypto/ec/src/bp256r1_comb_table.rs new file mode 100644 index 000000000..5a64c4c0e --- /dev/null +++ b/crypto/ec/src/bp256r1_comb_table.rs @@ -0,0 +1,207 @@ +//! The checked-in fixed-base comb table for `[k]G` (brainpoolP256r1's base point `G`), width 6, +//! matching this crate's other curves' choice for fields over 250 bits. See +//! [`crate::bp256r1_comb`] for the multiplier that uses this table and the algorithm it +//! implements. +//! +//! `COMB_TABLE_X[i]`/`COMB_TABLE_Y[i]` are the affine `(x, y)` coordinates of the `i`-th table +//! entry, as little-endian `u64` limbs; entry `0` is the point at infinity (`x = y = 0`, which is +//! not a valid affine coordinate pair for any point on the curve, so it's an unambiguous sentinel +//! here). +//! +//! Regenerated and compared against these checked-in values by this module's own `tests` +//! submodule below, using the same construction a (not checked in) scratch example used -- so a +//! hand-edit or transcription error in this file cannot survive `cargo test`. A unit test rather +//! than an integration test because the table is deliberately `pub(crate)`, not exposed outside +//! the crate: see [`crate::p256_comb_table`]'s docs for the same reasoning. + +pub(crate) const COMB_TABLE_X: [[u64; 4]; 64] = [ + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0x3a4453bd9ace3262, 0xb9de27e1e3bd23c2, 0x2c4b482ffc81b7af, 0x8bd2aeb9cb7e57cb], + [0x3a86369d8f1b1b84, 0x7c3e4dfdeb152115, 0x8d730ac130ab006c, 0x81b13370b7253d72], + [0xbab899b454bb984d, 0xdc8d910071de4892, 0xe8be5ed3ecef5281, 0x608a7298ec554706], + [0xd0ea175c2d8e94ca, 0x8b9ce1b25f76cafc, 0xdbc6f24e88f420dc, 0x60e062486571c960], + [0x256bb07a61a04906, 0x0b10c8d2b473cec9, 0xaa7d82b05a619978, 0x8d8c18d931e4fc4c], + [0x44b4b45b206f1cd1, 0x29609dae4e7f44d1, 0x97ad2b9f2c9d25c6, 0x6e9849ea26cd5960], + [0x0186795468b7a9c0, 0xb96e1f123b9fded3, 0x81348812232cc262, 0x54a434a208abe5db], + [0x09ffb63b8d799c2d, 0x7b82757d8a693df5, 0x98a972ce4a159040, 0x245a28f898c1c6b3], + [0xa781a50d3601041f, 0x81909e4978974543, 0xc45e4740c3f48169, 0x9abd6cf6f26a614e], + [0x15137398384e94cd, 0x6d3ead2a1373f46d, 0xcabb6fac03e27523, 0xa28d53db717dceda], + [0xfa8ed2b04767a9cf, 0xbac22791b46071ba, 0x66ec9a903ae584a8, 0x7b034967429639fd], + [0x71ecf6da18d2dacb, 0x48d7a8f6dcbd1e08, 0xe7db3d2f6ae8c0f2, 0xa65d75823b3ac78d], + [0x28c875ad7d8628ae, 0x542cc9afa8bbe243, 0x85fcc5e030d23569, 0x77ab196a7bc03943], + [0xf9dc0d7f609de141, 0x9cdc2129f17dffc5, 0x883bc9b341b0dbee, 0x7786af963b57dce1], + [0x631b0eb41859c9e2, 0xfbb0e053e1b0edf7, 0x942341993accd784, 0x6057115f58754b7b], + [0x8fd1ea6f630273ba, 0x8a3e19cb24a2e4d6, 0x8fae09609f048ef4, 0x61d6ae0995b5c197], + [0x397249cba5c38650, 0x473791c66f1b4a13, 0x34b474fbd85fee81, 0x89c7fc22e4c7ed19], + [0x2cd0f810d2f310d7, 0x0486ce913cba2370, 0xcb341125f9139abd, 0x3283a923f091f51b], + [0x7ae66a7c2a199c47, 0xc9fb9118661ded82, 0xf34852dac24ab2ea, 0x3da513e74bdb0e54], + [0x73ffb26c65571474, 0xd22532949a4a7d1f, 0x57d23e6ca46a05a1, 0x5b2d3bff92596176], + [0x93cc95ac9c4fb57b, 0x77a8a881f113f0bd, 0x93f78c54e27098c7, 0x04fb9b9ee7b27137], + [0x06fc37839cce78b6, 0xb28c89a9f02dc78c, 0x9450293e5644f687, 0x7e3dbd1883f4b539], + [0x405ca3269cda17ce, 0xf50d0c7847cee038, 0xc7e8fcce877fcc42, 0x18a399fe613fb9af], + [0x8ac6f9144d751d98, 0x0991ee268f49c94b, 0x628d367acdfc2766, 0x02813b531e8fd074], + [0xb84db09c21e5c3d1, 0x2c9fab3fa8b48655, 0x4912b7a7abcd16dc, 0x1ef5e3683ec22b30], + [0x2a72a43dad054b76, 0x10ad777bdb0a0fa3, 0xb0b2ce5034e68c6d, 0xa5875a98ec9c01b9], + [0x244505a65070c3dc, 0x874d78215e35b6de, 0xcda4cef6eb65102a, 0x1de85009364ff676], + [0x3f37fa725eea9a4c, 0x437e49a7bd7b1984, 0x6f1e2b7fd4a7f9b9, 0x90a541430df07bc8], + [0x77f19a7e8f4da3f2, 0x7e50d069ec0fa054, 0xd1164d859aa21bfc, 0x607e54772847dc2b], + [0x20b810ff1120c32a, 0x04b5ce104f69e740, 0x5dcca19714425984, 0x21bfebe8143af397], + [0x3e28307892931c6f, 0xdb25add13709c43d, 0xf7c8f86bd4ca45ea, 0x1db7a27f3959166b], + [0x3cf3061cf546d47f, 0x419c3ab1675e9fba, 0xac0dc45a5a06bd4d, 0x27f61d28092ad0f8], + [0x0314b65e32168000, 0xff6eaf97e9f12b39, 0x4dd6a6272ae496d4, 0x9667cb82bbad4801], + [0x0b1f2cfbc7c00014, 0xe65663e58bc91a1e, 0x9e3419e6f87860d3, 0x7e3c2f8099d61671], + [0x3f320346bc22ba02, 0x75ef2e5fb1f89925, 0xa7667e1719f4c2f7, 0x9b3b324d04da04d6], + [0x4765f9656b47d3e0, 0x141982b066de5dec, 0xb4c9898cc3c44555, 0x394e482e9349cfc2], + [0x988438cd2c795a63, 0x7a1de3356ab9f69a, 0x59e104a6cc3d998b, 0x34f1a6a9425a45b7], + [0x9585a8a236d8b009, 0x67b89021355145c0, 0x7f55270684160d7e, 0x59ee07039e9976ff], + [0xd43c32e6d218cec0, 0x9ad5de553f7d5297, 0x014fbb7c5a69fab2, 0x16cd5d0e1306a8dc], + [0x01182a47397d7a73, 0xc54288cb6eae9823, 0x6a7e3da0fc895f80, 0x87a48e0b9ed54deb], + [0x21d3250a95983d01, 0xb6884b1bcacd196d, 0x7eef247025e60047, 0x50e3ca28fe54ab48], + [0xbfcaf66e763c36e7, 0xeed74f7137ae7404, 0xa41a5401405cf5ba, 0x686b6d968370337f], + [0xcadd8354279c06f2, 0x0b7b288964d2ad61, 0xe5e8b5df5910a526, 0x795aae4fb23de090], + [0x41220323cefefc57, 0x72be0fb5320b17c3, 0xadb5721a002cfeee, 0x51bdd252a0ea31d0], + [0x8cfe76501c4bc37b, 0xc49907f914b8ce7c, 0xefd4dc98e4d791de, 0x8389a7b35caa5d30], + [0x428825bbedeaab99, 0xc06900c69de3a905, 0xaae2bff25638d9a7, 0x18a718efe1fe47e6], + [0x9c52caf7cf16cc6f, 0x4e4b80c3a51373a4, 0x3ff56250999d4acc, 0x2cd01182a150130f], + [0x6e864816716d58a1, 0xa61f778991531812, 0x61bc8946e81c4b92, 0x66abcc2df313d586], + [0xee7d33124c850ac6, 0xe6ebb052b723dc0d, 0x5f95c3ef9d327274, 0x6e8c9d9ed6aacf75], + [0x0eee906899bb8cb2, 0x447a113f39f2db20, 0x873b76d2bfab1765, 0x38d114b7bfef5049], + [0x6d5131a89583463b, 0x597b827a9093a08d, 0xaf5ebfda6d0d71b9, 0x278f80bef4ef1e8d], + [0x3dd9198ae9180945, 0xbfc8669878d61cb6, 0x2683ac49f0d033d8, 0x57a9cf96dae5ea72], + [0x9142e38dfd0b78eb, 0xaa06438939cfaa07, 0x900f9d3095b195ca, 0x4da55f13767c3765], + [0x786a01e5cd51ae97, 0xb6ed8728b02200cc, 0x00c192eb9b0821cb, 0x35b15fa78dc28556], + [0x025db8ddaa176ea5, 0x0b129cc4f5b1c3c0, 0xbfed1560dcd7c884, 0x8609981c4fabe8ea], + [0xc8673a11bcd60f3d, 0xd024c638eb11bacf, 0x26259f06a793949a, 0x2c089ad1ba69c7fb], + [0xd96dfa3814555890, 0xfe7e4355dcab45fa, 0x05cd4e5e6e072066, 0x8a7b2308e8b489b1], + [0x3b2f94e5bb251d09, 0x2dc6de4f38131087, 0x12b9598588bb875e, 0x0ab5de80422f73fb], + [0x18d6202481ae81a7, 0x2ed98b16b6c0805d, 0xab7473d27f6234b2, 0x09bde872dcb32862], + [0x1a1fff1657279647, 0xa703e8a34828301a, 0x724ecc5ccedd74d8, 0x225faf4ebf100455], + [0x571305aa4c32b3a5, 0x36daa0ddb0b5ddda, 0x3df3bae75ed4af51, 0x4b9418798c855a13], + [0x0b0d2681405179e1, 0xa1ed1ca287b6d8ed, 0x713b664a188bcba3, 0x8cf3caaed23dd049], + [0x3d0b47dc05c9909a, 0x28e9dd742064959c, 0xbdc25d60b82f36bd, 0x366fc0b7de9e294a], +]; + +pub(crate) const COMB_TABLE_Y: [[u64; 4]; 64] = [ + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0x5c1d54c72f046997, 0xc27745132ded8e54, 0x97f8461a14611dc9, 0x547ef835c3dac4fd], + [0x366f9b838d3aef4d, 0x24edeaf1fe6b7372, 0x0eea463b51d24d24, 0x76d5232aca72cb23], + [0xb65ebecc16fcbe43, 0xfef8afa7e10db6c5, 0x845f244fb1f19bb4, 0xa8199d0eed359041], + [0x169fee39e137a624, 0x629404f0e09d01b1, 0x7ca2a2c838165fc8, 0x28e5753e1e2040d3], + [0xa04a69d87c4bfc2f, 0xc45de4b97538c9cf, 0xff3ad456ae72d32b, 0x0307b6da73eadbf8], + [0x208fcf33dcd9c940, 0xf5d7a048f99f0739, 0xca50e85859d17a32, 0x42d2efb577a5e9b7], + [0x10a4e04edd807162, 0xb623f408aab64acf, 0x3a119349c2d05360, 0x9fc297a8ad82fc63], + [0xc34ec428fcd5ca97, 0x9fff81b0952c01e5, 0xfaf22b1c554b8ff0, 0x076de49a5b8a5ee1], + [0x73a76e9c4bf4eed7, 0x09c30d43e312ef50, 0x45b4aec2b7cb08d3, 0x2a23224293d733d5], + [0x970905ad31a91e85, 0x519711809aef9ee0, 0xca02cea8bb3703ca, 0x375339dda98108e1], + [0xfec2e9b1a78becf6, 0xd165da61af3e8af8, 0x8a335393e2277f6f, 0x865bd62db4ecbb74], + [0xd75fa01eded8e8b1, 0xb19b2eb7f2b59a3f, 0x0900405ad000b6ae, 0x20f4b2c891b966d0], + [0xec91cb2f33ac98b0, 0xeef915be98242aff, 0xd86da94450ef40a6, 0x8e01d2cc4ffc7e95], + [0xc2bb3ae413839bbf, 0x3dabcb6192c6416e, 0x40c7a2a7eb2fa88a, 0x74b5bd469bf7b0ba], + [0xb695199d34d980bd, 0x8a10a77c659217b4, 0x3c409a84f13e115f, 0x5431cf1056392abd], + [0x555ff88fe1852a4e, 0x7a6618c1e2453d51, 0xcacad1480cfd7c6c, 0x220d8c081f33e283], + [0x3ef9e669efcd641c, 0xf23c4878ba9cddb8, 0x2e13e89907dcbb63, 0x8f0b31f1e98540b5], + [0x4d0fe4442e6e8697, 0xa445918e85592017, 0xba64e0f014684614, 0x558094766f614dc8], + [0x4ee21b912ea4e499, 0x01c37c9af83734f8, 0x3446cd3b6ef6a35f, 0x46df218fc1029c48], + [0x0f8f1b5f1fb63931, 0x79bf34d5facece8a, 0x291805708ccb2f8a, 0x75a37300df3c5fb5], + [0x436ba92465466c16, 0xa1b78a00a1c7657a, 0xced1cfe949f94a00, 0x5dcc826ca692ce5b], + [0xefa592ca7f680a9e, 0x9f5a367b3da0b0f9, 0xf318c293586c7250, 0x89591b6b5a81248b], + [0x45a42e03ffe9c13f, 0x81f29069dfae5975, 0x23c788598205570e, 0x6ededfe03b40b98b], + [0xd67984c103d3c12c, 0x0de20ac1ab591f16, 0xc61ad065b4c888e9, 0x308f62c363d5f2de], + [0x2e8ddfd8668a0ad4, 0x95a25f2f07bc4762, 0x39652a289054dea4, 0x657aa9ed934f4fbd], + [0x79585526cf977286, 0xf5e69560bb343c58, 0xff24c0037d2abd09, 0x436a6ddc800a872b], + [0x8ab135882befac01, 0xd1e0bf243c12ed67, 0xf0908ca3803f3f8e, 0x59f8079048a80e48], + [0x7bda8340742981b1, 0xbdfadb5a6d9840f0, 0x43df96be862eded5, 0x6613f7e219c80927], + [0xfc68e6cbf2cbae9a, 0xd8ed5d26264883ae, 0xd772d591b795a8e4, 0x2b1c569836c9eb8c], + [0x263f9de2cc7d369c, 0x8b005a67925c2b73, 0x4e01f0b5556a7d39, 0x6a8e3daaf162e30e], + [0xfe1d56c75812a539, 0x2e9917231212b54e, 0xcc4c46857c029ed8, 0x4370db0589b648e0], + [0x58354c656a369ffa, 0x3d4eb3215d003734, 0xf9fdd6215146096e, 0x1bddfee2b170bd34], + [0x3416c189d86a1854, 0x21a243013b08d0cc, 0xbca73955f4b00efe, 0x23aa6f70fae1fd1b], + [0xb0b25e6296436ed5, 0xa4ba8ee234b5b009, 0xd132abb70142f77e, 0x87ea2c368b253538], + [0x3b86cb1426052d9b, 0x73fbc4360265ce2d, 0x89e14bb4f4c2c600, 0x5c5583eb0db426f1], + [0xd79204d6907a0d43, 0xa0d4d553a3ba5342, 0x1d9e9bf3009d3562, 0x3bd1fa37f0ac1897], + [0xec95467fae693d45, 0xc676bd083602e4e9, 0x15994d48a7c2f57a, 0x47373e95c8224e11], + [0x6de30d2613acc48a, 0xf451dbfea4570918, 0x4c7f7ec230444d53, 0x2a6be7261ffa2058], + [0xbc16d33267c2f66c, 0x6af42a93a5c6ec91, 0xe658ee29fae87dc0, 0xa6c89865f7c477c9], + [0xec2f00d1d9af1451, 0x0eaf1412de80df09, 0x7f4c984b26d0fcdb, 0x76f4f6496565ce1e], + [0x82a49dd285d1284b, 0x202faad8e7fc4d81, 0xe707b91ac3c6361c, 0x3e054332ff577329], + [0x91e6559bf409dad3, 0xa0bebebe6f4faa5b, 0xc1d3eb951486d7d0, 0x1a3100170a3e77c6], + [0x6dc70f93d833765a, 0xeb35b5359c13db58, 0xa7a4338284252df1, 0x076ee237bff22244], + [0xa4b029149128a831, 0xf4c3ff529be10db7, 0xb65f80d08837595b, 0x4363e27ff5fdc00e], + [0x13c71b1006b82f29, 0xbfa786f918b91d4e, 0x76d43fb136bc322e, 0x390040c95c04f4aa], + [0x60f8fd9e22a5c4ee, 0xb18a39c4555348c7, 0xcd81cff449d5650c, 0x9efc3ddb016fe144], + [0x1450934d7b3024e7, 0x388829e8fb0033f0, 0x664e99d9cfb9e5d9, 0xa404f070d00a8a57], + [0x9262df0e24b4a565, 0xd2b259b3cd6855f0, 0x411856005a628ac0, 0xa2899020f960e224], + [0xdb0c6ad6a820e0c6, 0x5bc62a737ac36726, 0x11ecd269b43e8a03, 0x04c90f1259fb7546], + [0x4726cb3fcab86834, 0x2913e851d6db02a1, 0x955308736c8fa143, 0x2207cacf7690deb2], + [0x70922a1105f37dce, 0x112a1b8fa348eea0, 0x5f9f7007e99e16e2, 0x2dcb264b89655594], + [0x36a947bec054ca7e, 0xff7c41507d8e9103, 0xe44e58803bab4301, 0x39c7f00b29ced75c], + [0x5b256721d827f9e9, 0xdce677de013851ab, 0x6b08f88f04f051f0, 0x30e2f57a847d2c68], + [0x4fada715afd4fb13, 0xd4c1919dd0742fb4, 0x6314d78d0996b536, 0x33f6c10847515c10], + [0x3ff9826c9200ef55, 0xca469b59e283fc05, 0x7a914bf341595b03, 0x0cdbdef95612836e], + [0x60ced74dd70d5d56, 0x87a24976da0c9708, 0x1184fadb7a1e029d, 0x33d0e6ef7f51a52e], + [0x676e41b1152df8f1, 0x62b615e22f9ac122, 0x45f85ddebd4dd527, 0x1b15c58ac4e5cbb0], + [0x1a8645f1b7e05541, 0x8bee43b0a0aecac0, 0x63ce88cd70ebfb43, 0x6c6ff37d938bfffe], + [0xf60f2ff584a36f6b, 0xca5b56110f8e297f, 0xcd9f0edbc9ef7883, 0x376040fda21c8225], + [0xa8bcbff94295a6f0, 0xa286037a0821c83e, 0x65d78840e2258ce9, 0x542376afe2ed2594], + [0x7d93766939154944, 0xf59d6f8faba73b9a, 0xa6951bd8dbef9dbf, 0x72e8ee7c540e1d44], + [0xc6c76a985dfe1675, 0x3e4be1378cbdec14, 0xdde69c31fa3c6cb1, 0x24d48a1e5ef7480d], + [0x34c4152ac2a6f514, 0x55347c801ca03615, 0xc99a5d5f9f828edf, 0x8216fc4adb16a928], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::bp256r1::Bp256r1FieldElement; + use crate::bp256r1_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::bp256r1_point::Bp256r1JacobianPoint; + + const WIDTH: usize = 6; + const BITS: usize = 256; + const D: usize = BITS.div_ceil(WIDTH); + const TABLE_SIZE: usize = 1 << WIDTH; + + /// Regenerates the comb table from scratch via the crate's own (independently verified) + /// point arithmetic, per this module's construction: `pow2[i] = 2^(i*D) * G`, then + /// `table[idx] = sum of pow2[b] for each bit b set in idx`. + #[test] + fn regenerated_table_matches_checked_in_constants() { + let g = Bp256r1JacobianPoint::from_affine( + Bp256r1FieldElement::from_limbs(G_X_LIMBS), + Bp256r1FieldElement::from_limbs(G_Y_LIMBS), + ); + + let mut pow2 = [g; WIDTH]; + for i in 1..WIDTH { + let mut p = pow2[i - 1]; + for _ in 0..D { + p = p.double(); + } + pow2[i] = p; + } + + let mut table = [Bp256r1JacobianPoint::INFINITY; TABLE_SIZE]; + for bit in (0..WIDTH).rev() { + let step = 1usize << bit; + let pw = pow2[bit]; + let mut i = step; + while i < TABLE_SIZE { + table[i] = table[i - step].add(&pw); + i += 2 * step; + } + } + + for (idx, point) in table.iter().enumerate() { + if idx == 0 { + assert!(point.is_infinity().to_bool(), "table[0] must be infinity"); + assert_eq!(COMB_TABLE_X[0], [0, 0, 0, 0]); + assert_eq!(COMB_TABLE_Y[0], [0, 0, 0, 0]); + } else { + let (x, y) = point + .to_affine() + .unwrap_or_else(|| panic!("regenerated table[{idx}] is unexpectedly infinity")); + assert_eq!(x.to_limbs(), COMB_TABLE_X[idx], "table[{idx}].x mismatch"); + assert_eq!(y.to_limbs(), COMB_TABLE_Y[idx], "table[{idx}].y mismatch"); + } + } + } +} diff --git a/crypto/ec/src/bp256r1_domain.rs b/crypto/ec/src/bp256r1_domain.rs new file mode 100644 index 000000000..3669a7fb8 --- /dev/null +++ b/crypto/ec/src/bp256r1_domain.rs @@ -0,0 +1,28 @@ +//! brainpoolP256r1 domain parameters, RFC 5639 §3.4. `p` lives in [`crate::bp256r1`]; `n` lives in +//! [`crate::bp256r1_scalar`]. This module holds the rest: the curve coefficients `A` and `B` and +//! the base point `G`. +//! +//! Unlike every NIST curve or secp256k1 in this crate, brainpool's `A` is neither `-3` nor `0`, so +//! it has no dedicated doubling-formula shortcut: [`crate::bp256r1_point`] uses the general-`a` +//! "dbl-2007-bl" formula instead. +//! +//! Every value below was extracted directly from a freshly downloaded copy of RFC 5639 §3.4's text +//! (concatenating the line-wrapped hex under `Curve-ID: brainpoolP256r1`, not retyped) and verified +//! in Python before any of this was ported to Rust: `y^2 = x^3 + Ax + B (mod p)` holds for `(G_X, +//! G_Y)`. + +/// The curve coefficient `A` in `y^2 = x^3 + Ax + B`, little-endian `u64` limbs. +pub const A_LIMBS: [u64; 4] = + [0xe94a4b44f330b5d9, 0xfb8055c126dc5c6c, 0xeef67530417affe7, 0x7d5a0975fc2c3057]; + +/// The curve coefficient `B` in `y^2 = x^3 + Ax + B`, little-endian `u64` limbs. +pub const B_LIMBS: [u64; 4] = + [0x6bccdc18ff8c07b6, 0x958416295cf7e1ce, 0xf330b5d9bbd77cbf, 0x26dc5c6ce94a4b44]; + +/// The base point `G`'s `x` coordinate, little-endian `u64` limbs. +pub const G_X_LIMBS: [u64; 4] = + [0x3a4453bd9ace3262, 0xb9de27e1e3bd23c2, 0x2c4b482ffc81b7af, 0x8bd2aeb9cb7e57cb]; + +/// The base point `G`'s `y` coordinate, little-endian `u64` limbs. +pub const G_Y_LIMBS: [u64; 4] = + [0x5c1d54c72f046997, 0xc27745132ded8e54, 0x97f8461a14611dc9, 0x547ef835c3dac4fd]; diff --git a/crypto/ec/src/bp256r1_extra_bits.rs b/crypto/ec/src/bp256r1_extra_bits.rs new file mode 100644 index 000000000..33733dfc6 --- /dev/null +++ b/crypto/ec/src/bp256r1_extra_bits.rs @@ -0,0 +1,54 @@ +//! FIPS 186-5 Appendix A.4.1, "Conversion of a Bit String to an Integer mod n via Modular +//! Reduction" -- the "extra random bits" method both `bouncycastle-ecdsa`'s `keys_bp256r1`'s key generation +//! (Appendix A.2.1) and `bouncycastle-ecdsa`'s `ecdsa_bp256r1`'s randomised per-message secret (Appendix A.3.1) +//! use to turn a DRBG output wider than `n` into a value in `[1, n-1]` with negligible bias. +//! Identical algorithm to [`crate::p256_extra_bits`] -- see that module's docs for the full reasoning +//! -- with brainpoolP256r1's own `n` substituted. +//! +//! # Why brainpoolP256r1 needs this at all +//! +//! Unlike P-384/P-521/secp256k1 (whose `n` is so close to a power of two that Appendix A.4.1's +//! bias bound holds with no extra bits, and which draw them only because Appendix A.3.1 demands +//! `t >= 64` -- see `crate::p384_extra_bits`'s docs), brainpoolP256r1's `n` is not +//! close to a power of two: `2^256 - n` is about a third of `2^256`, not a small remainder. FIPS +//! 186-5 Appendix A.4.1 step 2's bias-bound check (`2ρ(1-ρ)(n-1) > ε·N`, `ε = 2^-64`) was +//! evaluated directly (in Python, against `n`'s actual value, not assumed by analogy to any other +//! curve) for `N = 2^l` at increasing `l`: `l = 256` (matching `n`'s own bit length, P-384/P-521/ +//! secp256k1's shortcut) fails the check, and the smallest passing `l` is `319`; this crate uses +//! `l = 320` (40 bytes, one bit of margin above the minimum, byte-aligned for DRBG output) as +//! `bouncycastle-ecdsa`'s `keys_bp256r1::EXTRA_BITS_DRBG_OUTPUT_LEN`. +//! +//! The reduction itself is [`crate::barrett`]'s constant-time Barrett reduction with +//! `m = n - 1` (see [`crate::p256_extra_bits`]'s docs for the constant-time argument); `bp256r1_extra_bits_tests.rs` +//! pins known answers computed in Python and cross-checks it against the bit-serial algorithm this +//! module used to implement, kept there as an independent reference. + +use crate::barrett; +use crate::bp256r1_scalar::{Bp256r1Scalar, N_LIMBS}; +use crate::nat; + +/// `n - 1`, little-endian `u64` limbs. `N_LIMBS[0]` is odd (`n` is prime), so the subtraction +/// never borrows out of the low limb. +const N_MINUS_1_LIMBS: [u64; 4] = [N_LIMBS[0] - 1, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]]; + +/// `floor(2^512 / (n-1)) - 2^256`: the low limbs of Barrett's `mu` for `m = n - 1` (whose top +/// limb is `mu`'s only other bit -- see [`crate::barrett`]), computed in Python from +/// RFC 5639 §3.4's `n`. +const MU_LOW_LIMBS: [u64; 4] = + [0x50d73b46ccd10718, 0x9bf0088c5fdf55ea, 0xbb73aba8322a7bf4, 0x818c1131a1c55b7e]; + +/// FIPS 186-5 Appendix A.4.1 steps 3-5 for `n` = the curve order: reduces the big-endian bit string +/// `bytes` modulo `n-1` and adds `1`, landing in `[1, n-1]`. `bytes` may be up to `63` bytes +/// -- `2^504 < (n-1) * 2^256`, Barrett's precondition -- which covers the 40-byte DRBG +/// output `bouncycastle-ecdsa` draws with room to spare; a longer input is a programming error and panics. +pub fn reduce_wide_bits_mod_n_minus_1(bytes: &[u8]) -> Bp256r1Scalar { + assert!( + bytes.len() < 64, + "reduce_wide_bits_mod_n_minus_1 given {} bytes, more than 63", + bytes.len() + ); + let t = barrett::limbs_from_be_bytes::<8>(bytes); + let reduced = barrett::reduce::<4, 8, 5>(&t, &N_MINUS_1_LIMBS, &MU_LOW_LIMBS); + let (plus_one, _) = nat::add(&reduced, &[1, 0, 0, 0]); + Bp256r1Scalar::from_limbs(plus_one) +} diff --git a/crypto/ec/src/bp256r1_mul.rs b/crypto/ec/src/bp256r1_mul.rs new file mode 100644 index 000000000..c14440177 --- /dev/null +++ b/crypto/ec/src/bp256r1_mul.rs @@ -0,0 +1,100 @@ +//! Constant-time variable-base scalar multiplication: `[k]Q` for a secret `k` and a point `Q` +//! that is not the base point -- a peer's public key, for the ECC CDH primitive of SP 800-56A +//! Rev. 3 Section 5.7.1.2 (`P = h d_A Q_B`). +//! +//! The crate's other two multipliers do not cover this case: [`crate::bp256r1_comb`] is fixed-base +//! (`[k]G` from a compile-time table of multiples of `G`), and [`crate::bp256r1_wnaf`] takes +//! [`crate::bp256r1_scalar::Bp256r1PublicScalar`] and is variable-time by design. This one takes +//! [`Bp256r1Scalar`], the secret type, and is branch-free in `k` throughout. +//! +//! # Fixed window, width 4 +//! +//! `[k]Q` is computed left to right over `ROUNDS = ceil(BITS / 4)` windows of 4 bits: each round +//! doubles the accumulator four times and adds `[w]Q` for the round's window value `w`, read off +//! `k`'s limbs at positions fixed by the loop (so *which* bits are read is public, only their +//! values are secret). `[w]Q` comes from a table of `[0]Q .. [15]Q` built per call in Jacobian +//! coordinates -- entry 0 is the identity, the rest by doubling and adding `Q` -- and is read by +//! `table_lookup`, which scans every entry under a mask rather than indexing by `w`, as the +//! comb's lookup does. The accumulator's addition is the complete [`Bp256r1JacobianPoint::add`]: it +//! masks in the identity, same-point and opposite-point cases, so the accumulator may be the +//! identity (it starts there and stays there through leading zero windows), a table entry may be +//! the identity (`w = 0`), and `Q` itself may be added to a multiple of `Q`; the comb's "no +//! offset" argument carries over unchanged. +//! +//! Cost: `4 * ROUNDS` doublings, `ROUNDS` full additions, `ROUNDS` masked scans of the table and +//! 14 point operations to build it: on P-256 about three times the fixed-base comb (whose table +//! is precomputed and whose additions are mixed) and 1.3 times the variable-time wNAF multiplier +//! (136 us against 44 us and 106 us in `p256_point_benches` / `p256_scalar_benches`). The table +//! is `16 * size_of::()` bytes of stack. Width 4 over width 5 keeps +//! that table at 1.5 KiB here and 3.4 KiB on P-521 for one fewer addition in five; a Montgomery +//! ladder would need an addition per bit. The table and the working accumulator are scrubbed +//! before returning; the caller owns the result and scrubs it when it is done. +//! +//! Verified against [`crate::bp256r1_comb`] (for `Q = G`), against [`crate::bp256r1_wnaf`] and against +//! `[a k mod n]G` (for `Q = [a]G`) in `bp256r1_mul_tests.rs`, including `k = 0`, `k = 1`, `k = n-1`, +//! `Q` the identity, `Q` negated and `Q` in a non-canonical-`Z` representation. + +use crate::bp256r1_point::{Bp256r1JacobianPoint, select_point}; +use crate::bp256r1_scalar::Bp256r1Scalar; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 4; +const BITS: usize = 256; +const ROUNDS: usize = BITS.div_ceil(WIDTH); +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]Q`, branch-free in `k`. `Q` may be any point, including the identity; `k` may be any +/// value in `[0, n-1]`, and `[0]Q` is the identity. +pub fn multiply_point(k: &Bp256r1Scalar, q: &Bp256r1JacobianPoint) -> Bp256r1JacobianPoint { + let mut table = [Bp256r1JacobianPoint::INFINITY; TABLE_SIZE]; + table[1] = *q; + for i in 2..TABLE_SIZE { + // Which entry is built how depends only on the (public) index, never on `k` or `Q`. + table[i] = if i % 2 == 0 { table[i / 2].double() } else { table[i - 1].add(q) }; + } + + let limbs = k.limbs(); + let mut r = Bp256r1JacobianPoint::INFINITY; + for round in 0..ROUNDS { + let low_bit = (ROUNDS - 1 - round) * WIDTH; + let window = window_at(limbs, low_bit); + for _ in 0..WIDTH { + r = r.double(); + } + let mut entry = table_lookup(&table, window); + r = r.add(&entry); + entry.zeroize(); + } + + for entry in table.iter_mut() { + entry.zeroize(); + } + r +} + +/// The `WIDTH` bits of `limbs` from bit `low_bit` up, as a window value in `0..TABLE_SIZE`. Bits +/// at or above `BITS` read as zero, so on a curve whose `BITS` is not a multiple of `WIDTH` +/// (P-521) the top window sees only the scalar. The bit positions are public; the shifts and +/// masks are branch-free in the limb values. +fn window_at(limbs: &[u64; 4], low_bit: usize) -> usize { + let mut window = 0usize; + for j in (low_bit..low_bit + WIDTH).rev() { + let bit = if j < BITS { (limbs[j / 64] >> (j % 64)) & 1 } else { 0 }; + window = (window << 1) | (bit as usize); + } + window +} + +/// Table entry `secret_index`, selected by scanning every entry under a mask so the index never +/// steers which memory is read. +fn table_lookup( + table: &[Bp256r1JacobianPoint; TABLE_SIZE], + secret_index: usize, +) -> Bp256r1JacobianPoint { + let mut r = Bp256r1JacobianPoint::INFINITY; + for (i, entry) in table.iter().enumerate() { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + r = select_point(is_this_entry, entry, &r); + } + r +} diff --git a/crypto/ec/src/bp256r1_point.rs b/crypto/ec/src/bp256r1_point.rs new file mode 100644 index 000000000..cd5592e5b --- /dev/null +++ b/crypto/ec/src/bp256r1_point.rs @@ -0,0 +1,397 @@ +//! Branch-free Jacobian-coordinate point arithmetic on the brainpoolP256r1 curve `y^2 = x^3 + Ax + +//! B` (RFC 5639 §3.4). Unlike every NIST curve (`a = -3`) or secp256k1 (`a = 0`) in this crate, +//! brainpool's `A` is a "random" value with neither shortcut available. +//! +//! [`Bp256r1JacobianPoint::add`]'s exceptional-case handling (infinity, same-point, opposite-point) +//! is identical to [`crate::p256_point::P256JacobianPoint::add`] -- see that module's docs for the +//! full derivation and verification methodology, which applies unchanged here, since `generic_add` +//! doesn't depend on `a`. Only [`Bp256r1JacobianPoint::double`] differs: this is the general-`a` +//! doubling formula ("dbl-2007-bl" in the hyperelliptic.org explicit-formulas database): `XX = +//! X1^2`, `YY = Y1^2`, `YYYY = YY^2`, `ZZ = Z1^2`, `S = 2*((X1+YY)^2-XX-YYYY)`, `M = 3*XX + +//! a*ZZ^2`, `T = M^2 - 2*S`, `X3 = T`, `Y3 = M*(S-T) - 8*YYYY`, `Z3 = (Y1+Z1)^2 - YY - ZZ`, +//! verified (not checked in) against the affine doubling law over 500 random on-curve points, +//! using brainpoolP256r1's actual `A`, before being ported here. + +use crate::bp256r1::Bp256r1FieldElement; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// The curve coefficient `A` in the field's Montgomery representation (`A * 2^256 mod p`), +/// computed in Python from RFC 5639 §3.4's `A` and pinned against +/// [`Bp256r1FieldElement::from_limbs`] by this module's tests. [`Bp256r1JacobianPoint::double`] +/// needs `A` on every call; converting the plain `A_LIMBS` into Montgomery form each time cost a +/// full field multiplication per doubling. +const A_MONTGOMERY_LIMBS: [u64; 4] = + [0xd5d18edf69696261, 0xa68123f1c1d20c64, 0x95ec1e5e6398556e, 0x1e4676abd666bc17]; + +/// A point on the brainpoolP256r1 curve in Jacobian coordinates. +#[derive(Clone, Copy, Debug)] +pub struct Bp256r1JacobianPoint { + /// `X`, the Jacobian `X` coordinate. + pub x: Bp256r1FieldElement, + /// `Y`, the Jacobian `Y` coordinate. + pub y: Bp256r1FieldElement, + /// `Z`, the Jacobian `Z` coordinate; `Z == 0` denotes the point at infinity. + pub z: Bp256r1FieldElement, +} + +impl Bp256r1JacobianPoint { + /// The point at infinity, the identity of the curve's group. + pub const INFINITY: Self = Self { + x: Bp256r1FieldElement::ONE, + y: Bp256r1FieldElement::ONE, + z: Bp256r1FieldElement::ZERO, + }; + + /// Builds a Jacobian point from affine coordinates (`Z = 1`). Does not check that `(x, y)` is + /// actually on the curve -- see [`crate::bp256r1_sec1`] for that. + pub fn from_affine(x: Bp256r1FieldElement, y: Bp256r1FieldElement) -> Self { + Self { x, y, z: Bp256r1FieldElement::ONE } + } + + /// TRUE iff this is the point at infinity. + pub fn is_infinity(&self) -> Condition { + self.z.is_zero() + } + + /// Converts to affine `(x, y)` coordinates, or `None` for the point at infinity. Not constant + /// time: see [`crate::p256_point::P256JacobianPoint::to_affine`]'s docs. + pub fn to_affine(&self) -> Option<(Bp256r1FieldElement, Bp256r1FieldElement)> { + if self.is_infinity().to_bool() { + return None; + } + let z_inv = self.z.invert(); + let z_inv2 = z_inv.mul(&z_inv); + let z_inv3 = z_inv2.mul(&z_inv); + Some((self.x.mul(&z_inv2), self.y.mul(&z_inv3))) + } + + /// `-self`: negating the `Y` coordinate. + pub fn negate(&self) -> Self { + Self { x: self.x, y: self.y.negate(), z: self.z } + } + + /// Overwrites all three coordinates with zero (which is not the identity's encoding), for + /// scrubbing a secret-derived point before it goes out of scope; see + /// [`Bp256r1FieldElement::zeroize`]. + pub fn zeroize(&mut self) { + self.x.zeroize(); + self.y.zeroize(); + self.z.zeroize(); + } + + /// `2 * self`, via the general-`a` doubling formula ("dbl-2007-bl") described in the module + /// docs. + pub fn double(&self) -> Self { + let a = Bp256r1FieldElement::from_internal_limbs(A_MONTGOMERY_LIMBS); + let xx = self.x.mul(&self.x); // XX = X1^2 + let yy = self.y.mul(&self.y); // YY = Y1^2 + let yyyy = yy.mul(&yy); // YYYY = YY^2 + let zz = self.z.mul(&self.z); // ZZ = Z1^2 + let x1_plus_yy = self.x.add(&yy); + let s = x1_plus_yy.mul(&x1_plus_yy).sub(&xx).sub(&yyyy); + let s = s.add(&s); // S = 2*((X1+YY)^2 - XX - YYYY) + let m = xx.add(&xx).add(&xx).add(&a.mul(&zz.mul(&zz))); // M = 3*XX + a*ZZ^2 + let t = m.mul(&m).sub(&s).sub(&s); // T = M^2 - 2*S + let x3 = t; + let yyyy8 = + yyyy.add(&yyyy).add(&yyyy).add(&yyyy).add(&yyyy).add(&yyyy).add(&yyyy).add(&yyyy); + let y3 = m.mul(&s.sub(&t)).sub(&yyyy8); // Y3 = M*(S-T) - 8*YYYY + let y1_plus_z1 = self.y.add(&self.z); + let z3 = y1_plus_z1.mul(&y1_plus_z1).sub(&yy).sub(&zz); // Z3 = (Y1+Z1)^2 - YY - ZZ + Self { x: x3, y: y3, z: z3 } + } + + /// The generic (`self != ±other`, neither infinite) Jacobian addition formula, plus the `H` + /// and `R` intermediates [`Self::add`] uses to detect the exceptional cases. See + /// [`crate::p256_point::P256JacobianPoint`]'s private `generic_add`'s docs -- this formula + /// doesn't depend on `a` at all, so it's identical to the NIST curves' version. + fn generic_add(&self, other: &Self) -> (Self, Bp256r1FieldElement, Bp256r1FieldElement) { + let z2_sq = other.z.mul(&other.z); + let u1 = self.x.mul(&z2_sq); + let s1 = self.y.mul(&other.z).mul(&z2_sq); + let z1_sq = self.z.mul(&self.z); + let u2 = other.x.mul(&z1_sq); + let s2 = other.y.mul(&self.z).mul(&z1_sq); + + let h = u1.sub(&u2); + let r = s1.sub(&s2); + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); + let v = h_squared.mul(&u1); + let g_neg = g.negate(); + let acc = s1.mul(&g_neg); + let g2 = g_neg.add(&v).add(&v); + let x3 = r.mul(&r).sub(&g2); + let y3_temp = v.sub(&x3); + let y3 = acc.add(&y3_temp.mul(&r)); + let z3 = h.mul(&self.z).mul(&other.z); + + (Self { x: x3, y: y3, z: z3 }, h, r) + } + + /// `self + other`, branch-free per [`crate::p256_point::P256JacobianPoint::add`]'s docs. + pub fn add(&self, other: &Self) -> Self { + let self_is_infinity = self.is_infinity(); + let other_is_infinity = other.is_infinity(); + + let (generic, h, r) = self.generic_add(other); + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + // Dropping the `!` here (making `is_opposite_point` collapse onto `is_same_point`) is an + // accepted mutant, not a bug: when `h == 0`, `generic_add`'s own `z3 = h*self.z*other.z` + // is already `0` regardless of `r`, so `generic` is already infinity-representing in the + // true opposite-point case (`h == 0, r != 0`) even without this override selecting + // `Self::INFINITY` explicitly -- see `crate::p521_point`'s identical case for the full + // argument. + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for an `other` given in **affine** coordinates (`Z2 = 1`), or the identity + /// when `other_is_infinity` is TRUE (`x2`/`y2` are then ignored). The same group law and the + /// same branch-free discipline as [`Self::add`] -- generic-add and doubling candidates computed + /// unconditionally, the exceptional cases masked in -- but with `Z2 = 1` the generic formula + /// simplifies: `U1 = X1`, `S1 = Y1` and `Z3 = H * Z1`, which drops five of its sixteen field + /// multiplications. The fixed-base comb multiplier adds a table entry to its accumulator on + /// every round, and every table entry is affine or the identity, so it is the caller this + /// exists for; `pub(crate)` because a caller has to know its point really has `Z = 1`. + pub(crate) fn add_affine( + &self, + x2: &Bp256r1FieldElement, + y2: &Bp256r1FieldElement, + other_is_infinity: Condition, + ) -> Self { + let self_is_infinity = self.is_infinity(); + + // The generic mixed addition, computed unconditionally: `U1 = X1`, `S1 = Y1`. + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2 + let r = self.y.sub(&s2); // S1 - S2 + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); // R^2 + H^3 - 2V + let y3 = acc.add(&v.sub(&x3).mul(&r)); // -S1*H^3 + (V - X3)*R + let z3 = h.mul(&self.z); // H * Z1 * Z2 with Z2 = 1 + let generic = Self { x: x3, y: y3, z: z3 }; + + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + let other = Self { x: *x2, y: *y2, z: Bp256r1FieldElement::ONE }; + + // Same priority order as `add`: the identity cases override everything else. + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, &other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for points that are **public**, branching on the exceptional cases instead + /// of computing every candidate and masking. + /// + /// [`Self::add`] must evaluate the doubling candidate on every call, because on secret inputs + /// it cannot branch on whether the doubling case applies -- that costs a full point doubling + /// (roughly a third of the addition) on every addition, whether or not it is ever used. This + /// version pays it only when the operands really are the same point, which for a scalar + /// multiplier over distinct precomputed multiples is essentially never. + /// + /// Restricted to `pub(crate)` and used only by [`crate::bp256r1_wnaf`], whose scalars are + /// [`crate::bp256r1_scalar::Bp256r1PublicScalar`] and whose points are a signer's public key + /// and the curve's own base point: every value it branches on is already known to an attacker. + /// Do not call this on anything derived from a private key or a per-message secret -- the + /// constant-time [`Self::add`] exists for that, and the scalar type split is what keeps the two + /// multipliers from being confused for one another. + pub(crate) fn add_vartime(&self, other: &Self) -> Self { + if self.is_infinity().to_bool() { + return *other; + } + if other.is_infinity().to_bool() { + return *self; + } + + let (generic, h, r) = self.generic_add(other); + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + generic + } + /// `self + (x2, y2)` for a **public**, affine `other` (`Z2 = 1`): [`Self::add_vartime`]'s + /// early-exit structure over [`Self::add_affine`]'s mixed formula, for the variable-time + /// multiplier's fixed side, whose precomputed multiples of `G` are stored affine. + /// `pub(crate)` for both of the reasons its two parents are. + pub(crate) fn add_vartime_affine( + &self, + x2: &Bp256r1FieldElement, + y2: &Bp256r1FieldElement, + ) -> Self { + if self.is_infinity().to_bool() { + return Self::from_affine(*x2, *y2); + } + + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2, with U1 = X1 + let r = self.y.sub(&s2); // S1 - S2, with S1 = Y1 + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); + let y3 = acc.add(&v.sub(&x3).mul(&r)); + let z3 = h.mul(&self.z); // H * Z1, with Z2 = 1 + Self { x: x3, y: y3, z: z3 } + } +} + +pub(crate) fn select_point( + cond: Condition, + a: &Bp256r1JacobianPoint, + b: &Bp256r1JacobianPoint, +) -> Bp256r1JacobianPoint { + Bp256r1JacobianPoint { + x: Bp256r1FieldElement::from_internal_limbs(select_limbs( + cond, + &a.x.internal_limbs(), + &b.x.internal_limbs(), + )), + y: Bp256r1FieldElement::from_internal_limbs(select_limbs( + cond, + &a.y.internal_limbs(), + &b.y.internal_limbs(), + )), + z: Bp256r1FieldElement::from_internal_limbs(select_limbs( + cond, + &a.z.internal_limbs(), + &b.z.internal_limbs(), + )), + } +} + +fn select_limbs(cond: Condition, a: &[u64; 4], b: &[u64; 4]) -> [u64; 4] { + let mut out = [0u64; 4]; + ct::conditional_select(cond, a, b, &mut out); + out +} + +// `add_vartime` is `pub(crate)` -- deliberately unreachable from outside the crate, since calling +// it on a secret point would undo the constant-time discipline [`Bp256r1JacobianPoint::add`] exists +// for -- so no integration test can reach it. Its whole contract is that it computes the same group +// law as `add`, differing only in *how* it gets there, so that is what is pinned here: agreement on +// every exceptional case, including the ones a scalar multiplier over distinct precomputed +// multiples essentially never reaches by chance (a point added to itself, and a point added to its +// own negation). QUALITY_AND_STYLE's private-function carve-out applies. +#[cfg(test)] +mod tests { + use super::*; + use crate::bp256r1_domain::A_LIMBS; + + #[test] + fn a_montgomery_constant_is_a_in_montgomery_form() { + assert_eq!( + Bp256r1FieldElement::from_internal_limbs(A_MONTGOMERY_LIMBS), + Bp256r1FieldElement::from_limbs(A_LIMBS) + ); + } + use crate::bp256r1_domain::{G_X_LIMBS, G_Y_LIMBS}; + + #[test] + fn add_affine_agrees_with_add_on_every_case() { + let g = Bp256r1JacobianPoint::from_affine( + Bp256r1FieldElement::from_limbs(G_X_LIMBS), + Bp256r1FieldElement::from_limbs(G_Y_LIMBS), + ); + // A non-canonical-Z representation of G as well: `add_affine` reads `self.z`, so `self` + // must not be assumed affine even though `other` is. + let z = Bp256r1FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = Bp256r1JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + let selves = [g, g.double(), g.negate(), Bp256r1JacobianPoint::INFINITY, scaled_g]; + let others = [g, g.double(), g.negate()]; + for a in selves { + for b in others { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_affine(&bx, &by, Condition::::FALSE).to_affine(), + a.add(&b).to_affine(), + "add_affine disagrees with add" + ); + } + // The identity flag must win regardless of the coordinates passed alongside it. + assert_eq!( + a.add_affine(&g.x, &g.y, Condition::::TRUE).to_affine(), + a.to_affine(), + "add_affine with the identity flag must return self" + ); + } + } + + #[test] + fn add_vartime_affine_agrees_with_add_on_every_case() { + let g = Bp256r1JacobianPoint::from_affine( + Bp256r1FieldElement::from_limbs(G_X_LIMBS), + Bp256r1FieldElement::from_limbs(G_Y_LIMBS), + ); + let z = Bp256r1FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = Bp256r1JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + for a in [g, g.double(), g.negate(), Bp256r1JacobianPoint::INFINITY, scaled_g] { + for b in [g, g.double(), g.negate()] { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_vartime_affine(&bx, &by).to_affine(), + a.add(&b).to_affine(), + "add_vartime_affine disagrees with add" + ); + } + } + } + + #[test] + fn add_vartime_agrees_with_add_on_every_case() { + let g = Bp256r1JacobianPoint::from_affine( + Bp256r1FieldElement::from_limbs(G_X_LIMBS), + Bp256r1FieldElement::from_limbs(G_Y_LIMBS), + ); + // G, 2G, -G and the identity cover all four of `add`'s exceptional cases pairwise, plus + // the ordinary one (e.g. G + 2G). + let points = [g, g.double(), g.negate(), Bp256r1JacobianPoint::INFINITY]; + for a in points { + for b in points { + assert_eq!( + a.add_vartime(&b).to_affine(), + a.add(&b).to_affine(), + "add_vartime disagrees with add" + ); + } + } + } +} diff --git a/crypto/ec/src/bp256r1_scalar.rs b/crypto/ec/src/bp256r1_scalar.rs new file mode 100644 index 000000000..9f0e22e58 --- /dev/null +++ b/crypto/ec/src/bp256r1_scalar.rs @@ -0,0 +1,272 @@ +//! The brainpoolP256r1 scalar field, arithmetic mod the curve order `n` (RFC 5639 §3.4). Like +//! [`crate::bp256r1`], `n` here has no special algebraic structure, so this is built on +//! [`crate::montgomery`]'s generic Montgomery multiplication rather than a per-curve fold -- +//! identical in shape to every other curve's own scalar field in this crate (e.g. +//! [`crate::p256_scalar`]), just delegating the widening-multiply/REDC work to the shared module +//! instead of hand-writing it again. + +use crate::montgomery; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +/// `n`, the order of the brainpoolP256r1 base point `G`, little-endian `u64` limbs (RFC 5639 +/// §3.4). +pub const N_LIMBS: [u64; 4] = + [0x901e0e82974856a7, 0x8c397aa3b561a6f7, 0x3e660a909d838d71, 0xa9fb57dba1eea9bc]; + +/// `-n^-1 mod 2^64`, the Montgomery reduction constant. +const N_PRIME: u64 = 0xfbffbebdcbb40ee9; + +/// `R^2 mod n` (`R = 2^256`), used to bring a plain value into Montgomery form. +const R_SQUARED_LIMBS: [u64; 4] = + [0xe1d8d8de3312fca6, 0xf35d176a1134e4a0, 0x9b7f25e76c815cb0, 0x0b25f1b9c3236762]; + +/// `R mod n`, also the Montgomery representation of `1`. +const R_MOD_N_LIMBS: [u64; 4] = + [0x6fe1f17d68b7a959, 0x73c6855c4a9e5908, 0xc199f56f627c728e, 0x5604a8245e115643]; + +/// `n - 2`, little-endian `u64` limbs -- the public exponent [`Bp256r1ScalarField::invert`] +/// raises its base to. +const N_MINUS_2_LIMBS: [u64; 4] = + [0x901e0e82974856a5, 0x8c397aa3b561a6f7, 0x3e660a909d838d71, 0xa9fb57dba1eea9bc]; + +/// An element of the brainpoolP256r1 scalar field, i.e. an integer mod `n`. Stored internally in +/// Montgomery form; see the module docs. +#[derive(Clone, Copy, Debug)] +pub struct Bp256r1ScalarField([u64; 4]); + +impl Bp256r1ScalarField { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0]); + + /// The multiplicative identity. Its Montgomery form is `R mod n`, not the literal integer `1`. + pub const ONE: Self = Self(R_MOD_N_LIMBS); + + /// Builds a scalar-field element from little-endian `u64` limbs (an ordinary, non-Montgomery + /// value), reducing once if `limbs >= n` (valid since `limbs < 2^256 < 2n`), then converting + /// to Montgomery form. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + let (diff, borrow) = crate::nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + let t = montgomery::widening_mul::<4, 8>(&reduced, &R_SQUARED_LIMBS); + Self(Self::finish_redc(&t)) + } + + /// Returns the canonical little-endian `u64` limbs (an ordinary, non-Montgomery value), `< n`. + pub fn to_limbs(&self) -> [u64; 4] { + let mut t = [0u64; 8]; + t[..4].copy_from_slice(&self.0); + montgomery::redc::<4, 8, 9>(&t, &N_LIMBS, N_PRIME).0 + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + crate::nat::is_zero(&self.0) + } + + /// `self + other mod n`. Operates directly on the Montgomery-form limbs: no REDC needed. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = crate::nat::add(&self.0, &other.0); + let (sum_plus_r, _) = crate::nat::add(&sum, &R_MOD_N_LIMBS); + let (diff, borrow) = crate::nat::sub(&sum, &N_LIMBS); + let mut when_no_carry = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_r, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod n`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = crate::nat::sub(&self.0, &other.0); + let (corrected, _) = crate::nat::add(&diff, &N_LIMBS); + let mut result = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod n`. + pub fn negate(&self) -> Self { + let (diff, _) = crate::nat::sub(&N_LIMBS, &self.0); + let mut result = [0u64; 4]; + ct::conditional_select(self.is_zero(), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod n`, via Montgomery multiplication. + pub fn mul(&self, other: &Self) -> Self { + let t = montgomery::widening_mul::<4, 8>(&self.0, &other.0); + Self(Self::finish_redc(&t)) + } + + /// Overwrites this value with zero, through a write the compiler may not elide. + /// + /// For the signing path, where a value of this type holds `d`, `k` or `k^-1` and would + /// otherwise stay legible on the stack after the signature is returned. This type is `Copy` + /// and carries public values too (verification's `u`, `v`, `e`, `r`, `s`), so it is not + /// wrapped in [`bouncycastle_utils::secret::Secret`] -- that would cost its `Copy`, its `const + /// ZERO`/`ONE`, and a scrub on every intermediate including the public ones. The trade is that + /// scrubbing is the caller's job, on every path out of the function; see + /// `bouncycastle_ecdsa`'s `sign_with_k` for the intended use. + /// + /// This does not reach values the compiler kept in registers or spilled itself, and is + /// defence in depth rather than a guarantee. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`montgomery::widening_square`]. Used by the exponentiation loops, not by point + /// arithmetic; see [`crate::p256::P256FieldElement::square`] for the measurement behind that + /// split. + pub fn square(&self) -> Self { + let t = montgomery::widening_square::<4, 8>(&self.0); + Self(Self::finish_redc(&t)) + } + + /// Converts a secret scalar (`d` or `k`) into Montgomery form for field arithmetic. See + /// [`crate::p256_scalar::P256ScalarField::from_secret`]'s docs. + pub fn from_secret(secret: &Bp256r1Scalar) -> Self { + Self::from_limbs(*secret.limbs()) + } + + /// `self^-1 mod n`, or `0` if `self` is `0`. Fermat's little theorem (`n` is prime) over the + /// public exponent `n-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..4).rev() { + let limb = N_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } + + /// [`montgomery::redc`]'s `(high, extra)` result, reduced to the canonical `< n` value. See + /// [`crate::bp256r1::Bp256r1FieldElement`]'s identical `finish_redc` for the invariant this + /// relies on and how it was verified for this specific `n`. + fn finish_redc(t: &[u64; 8]) -> [u64; 4] { + let (high, extra) = montgomery::redc::<4, 8, 9>(t, &N_LIMBS, N_PRIME); + let (sum, _) = crate::nat::add(&high, &R_MOD_N_LIMBS); + let (diff, borrow) = crate::nat::sub(&high, &N_LIMBS); + let mut when_no_extra = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(borrow), + &high, + &diff, + &mut when_no_extra, + ); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(extra), + &sum, + &when_no_extra, + &mut result, + ); + result + } +} + +impl PartialEq for Bp256r1ScalarField { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. Comparing Montgomery-form limbs directly is valid: the map `x -> x*R mod n` is a + /// bijection. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..4 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for Bp256r1ScalarField {} + +fn reduce_once(limbs: [u64; 4]) -> [u64; 4] { + let (diff, borrow) = crate::nat::sub(&limbs, &N_LIMBS); + let mut result = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut result); + result +} + +/// A scalar mod `n`, `< n`, held in `Secret`. See [`crate::p256_scalar::P256Scalar`]'s docs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Bp256r1Scalar(Secret<[u64; 4]>); + +impl Bp256r1Scalar { + /// Builds a secret scalar from little-endian `u64` limbs, reducing once if `limbs >= n`. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + let mut secret = Secret::<[u64; 4]>::new(); + *secret = reduce_once(limbs); + Self(secret) + } + + /// Builds a secret scalar from SEC 1 §2.3.7 big-endian octets, reducing once if `>= n`. + pub fn from_be_bytes(bytes: &[u8; 32]) -> Self { + Self::from_limbs(crate::bp256r1_sec1::limbs_from_be_bytes(bytes)) + } + + /// Encodes to SEC 1 §2.3.7 big-endian octets. + pub fn to_be_bytes(&self) -> [u8; 32] { + crate::bp256r1_sec1::be_bytes_from_limbs(&self.0) + } + + /// The scalar's limbs, for the crate's own multiplier and field-arithmetic implementations to + /// read. Not exposed outside the crate. + pub(crate) fn limbs(&self) -> &[u64; 4] { + &self.0 + } +} + +/// A scalar mod `n`, `< n`, held as a plain (non-secret) value. See +/// [`crate::p256_scalar::P256PublicScalar`]'s docs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Bp256r1PublicScalar([u64; 4]); + +impl Bp256r1PublicScalar { + /// Builds a public scalar from little-endian `u64` limbs, reducing once if `limbs >= n`. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + Self(reduce_once(limbs)) + } + + /// Returns the scalar's little-endian `u64` limbs, `< n`. + pub fn to_limbs(&self) -> [u64; 4] { + self.0 + } + + /// `self^-1 mod n` in **variable time**, as a [`Bp256r1ScalarField`] ready for the arithmetic + /// that follows, or `0` for `0`. For ECDSA verification's `s` (FIPS 186-5 §6.4.2 step 4), + /// which is part of the signature and so already public; nothing derived from a private key + /// or a per-message secret may reach this, which is why it lives on the public scalar type + /// and not on [`Bp256r1ScalarField`], whose `invert` is the constant-time one. See + /// [`crate::inverse_vartime`]. + pub fn invert_vartime(&self) -> Bp256r1ScalarField { + Bp256r1ScalarField::from_limbs(crate::inverse_vartime::mod_inverse(&self.0, &N_LIMBS)) + } +} diff --git a/crypto/ec/src/bp256r1_sec1.rs b/crypto/ec/src/bp256r1_sec1.rs new file mode 100644 index 000000000..4a79cf704 --- /dev/null +++ b/crypto/ec/src/bp256r1_sec1.rs @@ -0,0 +1,187 @@ +//! SEC 1 v2.0 (Certicom, May 2009) §2.3.3/§2.3.4/§2.3.5/§2.3.7 point encoding, decoding, and octet- +//! string encoding for brainpoolP256r1. Identical in shape to [`crate::p256_sec1`] -- see that +//! module's docs for the full reasoning (SEC 1 §2.3.3/§2.3.4 cases, why there is no +//! point-at-infinity case, why partial validation is full validation here since RFC 5639 §3.4 +//! gives brainpoolP256r1 cofactor `h = 1`, and the compressed-decode square-root derivation) -- +//! with the curve equation `y^2 = x^3 + Ax + B` (a general `A`, per [`crate::bp256r1_point`], +//! instead of `a = -3`) and brainpoolP256r1's own constants (also `p ≡ 3 (mod 4)`, verified the +//! same way in this module's tests; RFC 5639 §3.4 gives both `p` and `h`). +//! +//! Field elements and scalars are encoded big-endian, fixed-width (§2.3.7's plain big-endian +//! integer encoding) -- the opposite byte order from this crate's internal little-endian `u64` +//! limbs, so [`limbs_from_be_bytes`] and [`be_bytes_from_limbs`] convert at the boundary. + +use crate::bp256r1::{Bp256r1FieldElement, P_LIMBS}; +use crate::bp256r1_domain::{A_LIMBS, B_LIMBS}; +use crate::bp256r1_point::Bp256r1JacobianPoint; +use crate::nat; + +/// `(p + 1) / 4`, the exponent [`decode`]'s compressed-point square root raises `α` to. +const SQRT_EXPONENT: [u64; 4] = + [0x0804d20747db94de, 0x9b8efd88f549880a, 0x0f9982a42760e35c, 0x2a7ed5f6e87baa6f]; + +/// Converts 32 big-endian bytes to little-endian `u64` limbs. +pub fn limbs_from_be_bytes(bytes: &[u8; 32]) -> [u64; 4] { + [ + u64::from_be_bytes(bytes[24..32].try_into().unwrap()), + u64::from_be_bytes(bytes[16..24].try_into().unwrap()), + u64::from_be_bytes(bytes[8..16].try_into().unwrap()), + u64::from_be_bytes(bytes[0..8].try_into().unwrap()), + ] +} + +/// Converts little-endian `u64` limbs to 32 big-endian bytes. +pub fn be_bytes_from_limbs(limbs: &[u64; 4]) -> [u8; 32] { + let mut out = [0u8; 32]; + out[0..8].copy_from_slice(&limbs[3].to_be_bytes()); + out[8..16].copy_from_slice(&limbs[2].to_be_bytes()); + out[16..24].copy_from_slice(&limbs[1].to_be_bytes()); + out[24..32].copy_from_slice(&limbs[0].to_be_bytes()); + out +} + +fn limbs_less_than_p(limbs: &[u64; 4]) -> bool { + let (_, borrow) = nat::sub(limbs, &P_LIMBS); + borrow == 1 +} + +/// `true` iff either coordinate is outside `[0, p)`. +fn either_out_of_range(x_limbs: &[u64; 4], y_limbs: &[u64; 4]) -> bool { + !limbs_less_than_p(x_limbs) || !limbs_less_than_p(y_limbs) +} + +/// `y^2 == x^3 + Ax + B`. +fn is_on_curve(x: &Bp256r1FieldElement, y: &Bp256r1FieldElement) -> bool { + let lhs = y.mul(y); + let x_cubed = x.mul(x).mul(x); + let ax = Bp256r1FieldElement::from_limbs(A_LIMBS).mul(x); + let rhs = x_cubed.add(&ax).add(&Bp256r1FieldElement::from_limbs(B_LIMBS)); + lhs == rhs +} + +/// `base^SQRT_EXPONENT mod p`. Plain (not masked) square-and-multiply: only ever runs on public +/// data (an incoming encoded point), like [`crate::p256_sec1::pow_public`]. +fn pow_public(base: &Bp256r1FieldElement, exponent_limbs: &[u64; 4]) -> Bp256r1FieldElement { + let mut result = Bp256r1FieldElement::ONE; + for limb_idx in (0..4).rev() { + let limb = exponent_limbs[limb_idx]; + for bit in (0..64).rev() { + result = result.square(); + if (limb >> bit) & 1 == 1 { + result = result.mul(base); + } + } + } + result +} + +/// SEC 1 §2.3.3 case 3: `04 || X || Y`, 65 bytes. +pub fn encode_uncompressed(x: &Bp256r1FieldElement, y: &Bp256r1FieldElement) -> [u8; 65] { + let mut out = [0u8; 65]; + out[0] = 0x04; + out[1..33].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out[33..65].copy_from_slice(&be_bytes_from_limbs(&y.to_limbs())); + out +} + +/// SEC 1 §2.3.3 case 2: `02 || X` or `03 || X`, 33 bytes, tag selected by `y mod 2`. +pub fn encode_compressed(x: &Bp256r1FieldElement, y: &Bp256r1FieldElement) -> [u8; 33] { + let mut out = [0u8; 33]; + out[0] = if y.to_limbs()[0] & 1 == 0 { 0x02 } else { 0x03 }; + out[1..33].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out +} + +/// Decodes and validates a SEC 1 §2.3.4 point encoding: 33 bytes (compressed) or 65 bytes +/// (uncompressed). See [`crate::p256_sec1::decode`]'s docs for exactly which checks this performs. +pub fn decode(bytes: &[u8]) -> Option<(Bp256r1FieldElement, Bp256r1FieldElement)> { + match bytes.len() { + 33 => { + let y_tilde: u64 = match bytes[0] { + 0x02 => 0, + 0x03 => 1, + _ => return None, + }; + let x_bytes: [u8; 32] = bytes[1..33].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + if !limbs_less_than_p(&x_limbs) { + return None; + } + let x = Bp256r1FieldElement::from_limbs(x_limbs); + let x_cubed = x.mul(&x).mul(&x); + let ax = Bp256r1FieldElement::from_limbs(A_LIMBS).mul(&x); + let alpha = x_cubed.add(&ax).add(&Bp256r1FieldElement::from_limbs(B_LIMBS)); + let beta = pow_public(&alpha, &SQRT_EXPONENT); + if beta.mul(&beta) != alpha { + return None; + } + let beta_parity = beta.to_limbs()[0] & 1; + let y = if beta_parity == y_tilde { beta } else { beta.negate() }; + Some((x, y)) + } + 65 => { + if bytes[0] != 0x04 { + return None; + } + let x_bytes: [u8; 32] = bytes[1..33].try_into().ok()?; + let y_bytes: [u8; 32] = bytes[33..65].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + let y_limbs = limbs_from_be_bytes(&y_bytes); + if either_out_of_range(&x_limbs, &y_limbs) { + return None; + } + let x = Bp256r1FieldElement::from_limbs(x_limbs); + let y = Bp256r1FieldElement::from_limbs(y_limbs); + if !is_on_curve(&x, &y) { + return None; + } + Some((x, y)) + } + _ => None, + } +} + +/// Decodes and validates an encoded public key (see [`decode`]) directly into a +/// [`Bp256r1JacobianPoint`]. +pub fn decode_point(bytes: &[u8]) -> Option { + let (x, y) = decode(bytes)?; + Some(Bp256r1JacobianPoint::from_affine(x, y)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn p_is_3_mod_4() { + assert_eq!(P_LIMBS[0] & 0b11, 3); + } + + #[test] + fn limbs_less_than_p_boundary_cases() { + assert!(limbs_less_than_p(&[0, 0, 0, 0])); + + let mut p_minus_1 = P_LIMBS; + p_minus_1[0] -= 1; + assert!(limbs_less_than_p(&p_minus_1)); + + assert!(!limbs_less_than_p(&P_LIMBS)); + + let mut p_plus_1 = P_LIMBS; + p_plus_1[0] += 1; + assert!(!limbs_less_than_p(&p_plus_1)); + + assert!(!limbs_less_than_p(&[u64::MAX; 4])); + } + + #[test] + fn either_out_of_range_requires_only_one_coordinate_to_be_out_of_range() { + let in_range = [0u64; 4]; + let out_of_range = P_LIMBS; + + assert!(!either_out_of_range(&in_range, &in_range)); + assert!(either_out_of_range(&out_of_range, &in_range)); + assert!(either_out_of_range(&in_range, &out_of_range)); + assert!(either_out_of_range(&out_of_range, &out_of_range)); + } +} diff --git a/crypto/ec/src/bp256r1_wnaf.rs b/crypto/ec/src/bp256r1_wnaf.rs new file mode 100644 index 000000000..1ec121838 --- /dev/null +++ b/crypto/ec/src/bp256r1_wnaf.rs @@ -0,0 +1,144 @@ +//! Interleaved windowed-NAF (wNAF) Shamir's-trick multiplication: `[u]G + [v]Q`, for +//! brainpoolP256r1 ECDSA verification. Identical algorithm to [`crate::p256_wnaf`] -- see that +//! module's docs for the full derivation and verification methodology -- with the limb count +//! swapped for brainpoolP256r1's (`WNAF_LEN = 257`, one more than `n`'s 256-bit length, same +//! margin as P-256's own `257 = 256 + 1`). +//! +//! `G`'s odd multiples are a compile-time affine table (`bp256r1_wnaf_table`, width 7) added by mixed +//! addition; only `Q`'s table is built per call. See [`crate::p256_wnaf`]. + +use crate::bp256r1::Bp256r1FieldElement; +use crate::bp256r1_point::Bp256r1JacobianPoint; +use crate::bp256r1_scalar::Bp256r1PublicScalar; +use crate::bp256r1_wnaf_table::{G_ODD_MULTIPLES_X, G_ODD_MULTIPLES_Y}; +use crate::nat; + +/// Window for `Q`, whose odd multiples are computed per call: 8 point additions to build the +/// table, one addition per ~6 digits to use it. +const WIDTH: usize = 5; +/// Window for `G`, whose odd multiples are a compile-time table +/// ([`G_ODD_MULTIPLES_X`]/[`G_ODD_MULTIPLES_Y`]): nothing to build, so the widest window a +/// signed `i8` digit allows, giving one addition per ~8 digits instead of ~6, and every one of +/// them a mixed addition against an affine entry. +const G_WIDTH: usize = 7; +const WNAF_LEN: usize = 257; +// Mutating this expression (e.g. `WIDTH - 2` -> `+ 2`) only grows the table `odd_multiples` builds +// and `lookup_signed` reads from; the actual digit range `compute_wnaf` can produce is bounded by +// `WIDTH` itself (unchanged), so the extra entries are simply never indexed -- an accepted mutant. +const ODD_MULTIPLE_COUNT: usize = 1 << (WIDTH - 2); + +/// `[u]G + [v]Q`. +pub fn shamir_multiply( + u: &Bp256r1PublicScalar, + v: &Bp256r1PublicScalar, + q: &Bp256r1JacobianPoint, +) -> Bp256r1JacobianPoint { + let du = compute_wnaf(u.to_limbs(), G_WIDTH); + let dv = compute_wnaf(v.to_limbs(), WIDTH); + + let table_q = odd_multiples(q); + + let mut r = Bp256r1JacobianPoint::INFINITY; + for i in (0..WNAF_LEN).rev() { + r = r.double(); + if let Some((x, y)) = lookup_signed_g(du[i]) { + r = r.add_vartime_affine(&x, &y); + } + if let Some(add_q) = lookup_signed(&table_q, dv[i]) { + r = r.add_vartime(&add_q); + } + } + r +} + +fn shr1(limbs: &[u64; 4]) -> [u64; 4] { + // Each `limbs[i] >> 1` only occupies bits 0-62 (its own bit 63 is shifted out), and + // `(limbs[i+1] & 1) << 63` only ever occupies bit 63 -- disjoint, so `|` and `^` agree here. + [ + (limbs[0] >> 1) | ((limbs[1] & 1) << 63), + (limbs[1] >> 1) | ((limbs[2] & 1) << 63), + (limbs[2] >> 1) | ((limbs[3] & 1) << 63), + limbs[3] >> 1, + ] +} + +/// The width-`width` wNAF digits of `k` (`width` at most 7, so a digit fits an `i8`), LSB-first; unused trailing entries are `0`. Not +/// constant time: only ever called on public scalars (see the module docs). +fn compute_wnaf(k_limbs: [u64; 4], width: usize) -> [i8; WNAF_LEN] { + let mut digits = [0i8; WNAF_LEN]; + let mut k = k_limbs; + let mut pos = 0; + while k != [0, 0, 0, 0] { + if k[0] & 1 == 1 { + // Mutating this mask (e.g. `(1 << WIDTH) - 1` -> `+ 1`) is an accepted mutant, not a + // bug: whatever mask is used, `digit` always keeps `k[0]`'s low (odd) bit, so it's + // always odd -- the only invariant `lookup_signed` and the subtract-or-add-then-`shr1` + // step below actually rely on. A degenerate mask just makes this compute a + // non-minimal (but still exactly reconstructing `k`) signed-digit sequence -- see + // `crate::p521_wnaf`'s identical case for the full argument and verification. + let mut digit = (k[0] & ((1 << width) - 1)) as i16; + if digit >= 1 << (width - 1) { + digit -= 1 << width; + } + k = if digit >= 0 { + let (new_k, borrow) = nat::sub(&k, &[digit as u64, 0, 0, 0]); + debug_assert_eq!(borrow, 0); + new_k + } else { + let (new_k, carry) = nat::add(&k, &[(-digit) as u64, 0, 0, 0]); + debug_assert_eq!(carry, 0); + new_k + }; + debug_assert!(pos < WNAF_LEN, "wNAF encoding overflowed its verified bound"); + digits[pos] = digit as i8; + } + k = shr1(&k); + pos += 1; + } + digits +} + +/// The odd multiples `1*p, 3*p, 5*p, ..., (2*ODD_MULTIPLE_COUNT - 1)*p`, indexed so entry `i` +/// holds `(2i+1)*p`. +fn odd_multiples(p: &Bp256r1JacobianPoint) -> [Bp256r1JacobianPoint; ODD_MULTIPLE_COUNT] { + let double_p = p.double(); + let mut table = [*p; ODD_MULTIPLE_COUNT]; + let mut cur = *p; + for entry in table.iter_mut() { + *entry = cur; + cur = cur.add_vartime(&double_p); + } + table +} + +/// `digit * p` where `p` is the point `table` was built from ([`odd_multiples`]), or `None` for +/// digit `0`. +fn lookup_signed( + table: &[Bp256r1JacobianPoint; ODD_MULTIPLE_COUNT], + digit: i8, +) -> Option { + // Every accepted-mutant survivor below relies on `digit` always being odd (it's built from an + // odd `k[0]` in `compute_wnaf`, see that function's docs): `digit > 0` vs `>= 0` agree once + // the `digit == 0` case is already handled above, and for odd `digit`, `(digit - 1) / 2 == + // digit / 2` (and symmetrically for `-digit`), since integer division already floors an odd + // numerator down to the same value subtracting 1 first would. + if digit == 0 { + None + } else if digit > 0 { + Some(table[(digit as usize - 1) / 2]) + } else { + Some(table[(-digit as usize - 1) / 2].negate()) + } +} + +/// `digit * G` for a width-[`G_WIDTH`] digit, read from the compile-time table of odd multiples +/// of `G` as affine coordinates (negating `y` for a negative digit), or `None` for digit `0`. +fn lookup_signed_g(digit: i8) -> Option<(Bp256r1FieldElement, Bp256r1FieldElement)> { + if digit == 0 { + return None; + } + let index = (digit.unsigned_abs() as usize - 1) / 2; + let x = Bp256r1FieldElement::from_limbs(G_ODD_MULTIPLES_X[index]); + let y = Bp256r1FieldElement::from_limbs(G_ODD_MULTIPLES_Y[index]); + Some(if digit > 0 { (x, y) } else { (x, y.negate()) }) +} diff --git a/crypto/ec/src/bp256r1_wnaf_table.rs b/crypto/ec/src/bp256r1_wnaf_table.rs new file mode 100644 index 000000000..7bce7cf45 --- /dev/null +++ b/crypto/ec/src/bp256r1_wnaf_table.rs @@ -0,0 +1,105 @@ +//! The checked-in odd multiples of the base point `G` for [`crate::bp256r1_wnaf`]'s fixed side: +//! `G_ODD_MULTIPLES_X[i]`/`G_ODD_MULTIPLES_Y[i]` are the affine coordinates of `(2i + 1) G`, for +//! `i` in `0..32`, as little-endian `u64` limbs -- the table a width-7 wNAF over `G` indexes. +//! `Q`'s table is built at run time because `Q` is the signer's key; `G` is the curve's constant, +//! so its table need not be. +//! +//! Generated in Python from the affine group law (SP 800-186 Appendix A.1.1) and RFC 5639 §3.4's `G`, +//! independently of this crate's arithmetic, and checked there by `n * G == infinity`; this +//! module's own test regenerates every entry with the crate's point arithmetic and compares. + +/// `x` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_X: [[u64; 4]; 32] = [ + [0x3a4453bd9ace3262, 0xb9de27e1e3bd23c2, 0x2c4b482ffc81b7af, 0x8bd2aeb9cb7e57cb], + [0x6b91e2ad25cae39d, 0xd2aa843d0c0fca01, 0xd6624c3ab4f6cc16, 0xa8f217b77338f1d4], + [0x32f95f7c85fe101d, 0x7cf41589c0d8c3fb, 0xa5f863e8b69fc147, 0x855433a3a4c8e334], + [0x02295ecd04e9de4c, 0xd432568e24e5fb57, 0x4d3300afbc27257b, 0x6b8bb7f53e36b682], + [0x420d27d6784f8f12, 0xd283bd714a67c06a, 0x0811364099019b7c, 0x8b5fa06d31d59d69], + [0x3406afffde3eeed0, 0x80c350b1e1db41b0, 0x8ddc9c5870ea1631, 0x50ea43e33d2d4897], + [0x62c969c86253556b, 0x3439bbf4e2b459b6, 0x7862ac771ce2cb74, 0x8d4243f928ee1b6a], + [0x7a309a0b21557a8e, 0x48e0917a7d5edc4b, 0xc6e07a490cee9078, 0x04306f8d5631ee7a], + [0x0b73fcb1a0c86824, 0xda7b65585bfbaada, 0xc3663e5f98b9dafa, 0x884d1f975768ce45], + [0xa58eab8ab0afbae7, 0x20411e949b5343f7, 0x8c6280c5f7796cdb, 0x09a299ed5649e1ea], + [0x237e790bb1a5b4ce, 0x3b759efc62bb5f35, 0x3df3221040023c85, 0x570308793e0c5eba], + [0xbe3b17da81484ed7, 0xabb09f7acc59db53, 0x7fa1ffadda5f3a40, 0x41c849b05a0d6a54], + [0xf2cf3ad83fa052c9, 0xca2fbfe7d1cf4e00, 0x2185ad28fb5fa0fa, 0x4e71767e126fd5f7], + [0xe2382f79baecc1e1, 0x0094f1814ba94c4a, 0x15b8777a954bcc12, 0x0274efabe5807d45], + [0x46359d211ac38cc5, 0x80cc84e37da25cc7, 0x50a3f267e294dabe, 0x7a55f5b14ca5987c], + [0x30ef11e45497bac8, 0xf01c0500489b5ccb, 0x76b7c208ad20eab9, 0x260f6273e0bb1dd8], + [0xd41037567accf8c7, 0xfe8ef690ae1eac5a, 0x7aa6f2cb689b376f, 0x4e8a6fb1464a8518], + [0x2ea157f038125580, 0x3fdc2bd0dfd3d44e, 0xfa072c5c90ca7465, 0x6f32d5afbb348dfa], + [0x43fc7544333f3428, 0x0f4531ed41d2adc4, 0x8529a7f04da9ed69, 0x17eb6c733c9784ef], + [0x9d581ef650cc6c04, 0x1756302344392682, 0x99285b38fafc2729, 0x88194fc15da5ecda], + [0xf3eee50460f0ee5d, 0x9f4fc06ae23480c0, 0x070ab8859b446ccd, 0x8c37a6089a705fc1], + [0xa0763644c6db33da, 0x177c596229b13603, 0xde882c05ab7a5053, 0x19640edeefc6afc5], + [0x2e9f0135784895cf, 0xbfd264c269ed642a, 0x93d20af87c0adbfa, 0x0458eb7922078146], + [0xf26f04d92940040e, 0x2feb9c8e911770b6, 0x0ed998e878d374d3, 0x47ac5f13da531200], + [0x4fd996642445cb4d, 0xe65418c7b7f9f4ec, 0x9dec0ebef91c595c, 0x3d7189f40aeb6063], + [0xa78381d11da54849, 0xc5b8caaf04ef1669, 0xd0d0e21d220cedc1, 0x9be3f9be5ff16ae8], + [0xf1f1e73b8e32065e, 0x85530598ae2af728, 0x83daf96c3b24bc90, 0x0fcbc30b72dc3e2c], + [0xb35ce8ffd1f97eb4, 0x83d561f5301ec598, 0xb5d449a454cb7242, 0x7b36877eabe731f9], + [0xd30ceea4d5193f47, 0x39b19246e1f92c95, 0x668a8f09295e9cb7, 0xa27bdb6299aa1753], + [0x845dd1b1e33c6da6, 0x28a1dfc821d87a4e, 0xe2cc6eb2ddc002cd, 0x9f98f97d6b809cb0], + [0x1d44cce166c0742d, 0xa03b399a33e4c3e0, 0xb61889c37cd95949, 0x226f783772b52dd3], + [0x58e6031bb4a5e4fc, 0x7e53f9040ffacb9a, 0x13a28e3ad3515448, 0x8daf7dc659e90919], +]; + +/// `y` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_Y: [[u64; 4]; 32] = [ + [0x5c1d54c72f046997, 0xc27745132ded8e54, 0x97f8461a14611dc9, 0x547ef835c3dac4fd], + [0x7e65cc5602b74f9d, 0xfac10e4589348fb7, 0x0aa2a6850a1b40f5, 0x4b49cafc7dac26bb], + [0xef9e224a5fd8814c, 0x097082129591c88b, 0xd7e172e40350d911, 0xa50c95efc2ad06c4], + [0x7252376bfc79ebf0, 0x646067c55b1a928f, 0x0965a09661223af5, 0x382f9af51ce9a3d3], + [0x42be764f293b3348, 0x146f15eea266b228, 0xae64ed13d26d038e, 0x41e0e0c34464b5c7], + [0x23af9bdadf6fbd40, 0x951970a9b39a21f9, 0x782129d70ceeb10f, 0x4685dceca1753941], + [0x616a664e902cb740, 0xf5c5afbb15e5c033, 0xc257d5e888eb9dad, 0x6cb4b54150658725], + [0x9377c731f7e833bd, 0xa510f13a83e146a2, 0x3aa032daf9ffd870, 0x2ab9e5213104bc7f], + [0x6205d05254c57ad7, 0x0f42120813fc7e96, 0x601705fe7f902e37, 0x569ea2dd9a21654f], + [0x14f5e940d854688a, 0x899aded511795131, 0xf82c6eb87b8812ea, 0x3ee027407a6089df], + [0xe7310d4c9ad17277, 0x02c6b0cc03e52178, 0x80dcefccfac71135, 0x4a2afc2717cbee53], + [0x6feea011fae28d88, 0xc2a8d275e501b318, 0x8fb15b04fc432fb9, 0x03f86f1566d23ff1], + [0xc7164f4db751f8f4, 0xb6d936b56f8ac707, 0x2c9eb4b82368af89, 0x6827f6a39886c9f8], + [0x4fc43228cb5be420, 0x109430909dc5454e, 0x3aae77d7197ac174, 0x9d81f55342ffe3fa], + [0xdd9924f09f478511, 0x0334cbf9014205e6, 0x30835f439129c742, 0x38f6f28e41e896c1], + [0x8ec0586d55f3a86a, 0x8afd97c0055dd2f4, 0x871017b8d86eccae, 0x6ebea505dc4e8a4f], + [0xa0633cdbd18858fc, 0xa024299cc4d2b54d, 0x49918b696473edc5, 0x50d931a544455a96], + [0xd4b2100477c436ac, 0xd48b49fb5dbbda9c, 0x1ef9f1f84331a57d, 0x5e7c2c02d8a53f05], + [0x3b72575e59ccfb75, 0x273f8849e60b8533, 0x07b2e53cecc34e82, 0x223e3e99fa8c7fc0], + [0xe7ba3b187454d62a, 0x8a86bd3fccf5fa6d, 0x189d8736683669ff, 0x2632e118005c0edc], + [0xa203d5682e4475b9, 0x98ac080c6ffc2adc, 0xf42e1a1de7fcc5fa, 0x685fa8d530d90f6f], + [0x9112c55e26717fa3, 0xbfa4c5e7044ff7b4, 0x23b70c0202174d77, 0xa8879bd92ba6a910], + [0x5b4dc05151f0fa08, 0x0661062acd60dc00, 0xfda33eac1ddd8d75, 0x64f15229d1edc963], + [0x186bd111c7da9fc4, 0x62106edf21b5bedf, 0x3580c7bb212b64db, 0x3117a2ce36cb6f90], + [0x84834d5ba8cbe26f, 0xf9a163bb2377d06e, 0x8e5fa5fd48471ef8, 0x1ab4a6eb82e36b8a], + [0xfab56200ee41e549, 0x79c97210d8925021, 0x137b0a84c93c4e5b, 0xa0e10494e748130a], + [0x7f9936771ec9a332, 0x3d3995d253730a86, 0x431b62fe6f2374b6, 0x37cce18a2c2a03f0], + [0x5c43dd1f9804f327, 0x7a9e378b3f12900b, 0xaa542e02960da127, 0x0dd61f45b230d121], + [0xb1539320dbf6474b, 0x940f9d84099b431d, 0xd9274ed922662c03, 0x4b1f12933e05cc11], + [0x6c38d88b1d2ddd50, 0x85040dcff7646ac9, 0x89e78260a3d3e596, 0x0594f4ce1f9c4e7d], + [0x61121c4be0575357, 0x90d9c3536c909029, 0xf441e7936cce52ee, 0x7c49c4aafe5ba327], + [0x71f7e4b9a4870fb5, 0xf3fce697d4d312f3, 0x240641d1fedc3aff, 0x6248a2224d790442], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::bp256r1::Bp256r1FieldElement; + use crate::bp256r1_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::bp256r1_point::Bp256r1JacobianPoint; + + #[test] + fn table_matches_the_odd_multiples_of_g_computed_by_the_point_arithmetic() { + let g = Bp256r1JacobianPoint::from_affine( + Bp256r1FieldElement::from_limbs(G_X_LIMBS), + Bp256r1FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + let mut cur = g; + for i in 0..32 { + let (x, y) = cur.to_affine().expect("an odd multiple of G below n is never infinity"); + assert_eq!(x.to_limbs(), G_ODD_MULTIPLES_X[i], "entry {i}: x of {}G", 2 * i + 1); + assert_eq!(y.to_limbs(), G_ODD_MULTIPLES_Y[i], "entry {i}: y of {}G", 2 * i + 1); + cur = cur.add_vartime(&two_g); + } + } +} diff --git a/crypto/ec/src/bp384r1.rs b/crypto/ec/src/bp384r1.rs new file mode 100644 index 000000000..0583fba29 --- /dev/null +++ b/crypto/ec/src/bp384r1.rs @@ -0,0 +1,233 @@ +//! The brainpoolP384r1 base field, GF(p) (RFC 5639 §3.4). Unlike every NIST curve or secp256k1 in +//! this crate, `p` here is a "random" prime with no Solinas-style algebraic shortcut for +//! reduction (RFC 5639 §3.1's curves are generated pseudorandomly, not chosen for a special +//! bit-pattern) -- so this field, like [`crate::bp384r1_scalar`], is built on +//! [`crate::montgomery`]'s generic Montgomery multiplication instead of a per-curve fold. +//! +//! `p`'s value and the base point's coordinates were extracted directly from RFC 5639 §3.4's text +//! (programmatically, concatenating the line-wrapped hex, not retyped) and verified in Python: `G` +//! is on the curve and `n·G` is the point at infinity, before any of this was ported to Rust. + +use crate::montgomery; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// `p`, little-endian `u64` limbs (RFC 5639 §3.4). +pub const P_LIMBS: [u64; 6] = [ + 0x874700133107ec53, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, +]; + +/// `-p^-1 mod 2^64`, the Montgomery reduction constant. +const N_PRIME: u64 = 0x9a6ea96cea9ec825; + +/// `R^2 mod p` (`R = 2^384`), used to bring a plain value into Montgomery form. +const R_SQUARED_LIMBS: [u64; 6] = [ + 0x087cefff40b64bde, 0x535283343d7fd965, 0x8e28f99cc9940899, 0x621401919918d5af, + 0xd5c6ef3ba57e052c, 0x36bf6883178df842, +]; + +/// `R mod p`, also the Montgomery representation of `1`. +const R_MOD_P_LIMBS: [u64; 6] = [ + 0x78b8ffeccef813ad, 0x532c58d66fe2e58e, 0xed4e25e68048eedc, 0xead08ef612aba94b, + 0xf0a29081af19be20, 0x7346e17d5cc792d7, +]; + +/// `p - 2`, little-endian `u64` limbs -- the public exponent [`Bp384r1FieldElement::invert`] +/// raises its base to, per Fermat's little theorem. +const P_MINUS_2_LIMBS: [u64; 6] = [ + 0x874700133107ec51, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, +]; + +/// An element of the brainpoolP384r1 base field GF(p). Stored internally in Montgomery form. +#[derive(Clone, Copy, Debug)] +pub struct Bp384r1FieldElement([u64; 6]); + +impl Bp384r1FieldElement { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0, 0, 0]); + + /// The multiplicative identity. Its Montgomery form is `R mod p`, not the literal integer `1`. + pub const ONE: Self = Self(R_MOD_P_LIMBS); + + /// Builds a field element from little-endian `u64` limbs (an ordinary, non-Montgomery value), + /// reducing once if `limbs >= p` (valid since `limbs < 2^384 < 2p`: `p`'s top limb is + /// `0x8cb9...`, comfortably more than half of `u64::MAX`, so `2p > 2^384`), then converting to + /// Montgomery form. + pub fn from_limbs(limbs: [u64; 6]) -> Self { + let (diff, borrow) = crate::nat::sub(&limbs, &P_LIMBS); + let mut reduced = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + let t = montgomery::widening_mul::<6, 12>(&reduced, &R_SQUARED_LIMBS); + Self(Self::finish_redc(&t)) + } + + /// Returns the canonical little-endian `u64` limbs (an ordinary, non-Montgomery value), `< p`. + pub fn to_limbs(&self) -> [u64; 6] { + let mut t = [0u64; 12]; + t[..6].copy_from_slice(&self.0); + montgomery::redc::<6, 12, 13>(&t, &P_LIMBS, N_PRIME).0 + } + + /// The limbs exactly as this type stores them: the Montgomery form `x*R mod p`, not the + /// integer `x`. Paired with [`Self::from_internal_limbs`] so the crate's point arithmetic can + /// select between the coordinates of two existing points without converting each one out of + /// and back into Montgomery form -- a REDC and a multiplication per coordinate, which is what + /// [`Self::to_limbs`] followed by [`Self::from_limbs`] costs. + pub(crate) fn internal_limbs(&self) -> [u64; 6] { + self.0 + } + + /// Wraps limbs already in this type's stored representation (see [`Self::internal_limbs`]), + /// skipping the reduction and conversion [`Self::from_limbs`] performs. Only for values that + /// came out of another element of this type, or a constant computed in that representation: + /// the masked selects in the point arithmetic, and the comb table's infinity sentinel. Not + /// exposed, since an out-of-range value would break every equality and reduction assumption + /// downstream; the range is checked in debug builds. + pub(crate) fn from_internal_limbs(limbs: [u64; 6]) -> Self { + debug_assert_eq!( + crate::nat::sub(&limbs, &P_LIMBS).1, + 1, + "from_internal_limbs given a value >= p" + ); + Self(limbs) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + crate::nat::is_zero(&self.0) + } + + /// Overwrites the element with zero, for scrubbing a secret-derived value before it goes out + /// of scope. Defence in depth rather than a guarantee: copies the compiler kept in registers + /// or spilled itself are not reached. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self + other mod p`. Operates directly on the Montgomery-form limbs: Montgomery form is + /// linear, so this needs no REDC, just the same carry-aware correction every Montgomery + /// scalar field in this crate uses -- `R_MOD_P_LIMBS` (`R mod p`), not `P_LIMBS`, since a + /// carry out of the top limb means the true sum is `sum + 2^384`, and `2^384 mod p` is + /// `R_MOD_P_LIMBS` by definition, not `p` itself. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = crate::nat::add(&self.0, &other.0); + let (sum_plus_r, _) = crate::nat::add(&sum, &R_MOD_P_LIMBS); + let (diff, borrow) = crate::nat::sub(&sum, &P_LIMBS); + let mut when_no_carry = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 6]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_r, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod p`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = crate::nat::sub(&self.0, &other.0); + let (corrected, _) = crate::nat::add(&diff, &P_LIMBS); + let mut result = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod p`. + pub fn negate(&self) -> Self { + let (diff, _) = crate::nat::sub(&P_LIMBS, &self.0); + let mut result = [0u64; 6]; + ct::conditional_select(self.is_zero(), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod p`, via Montgomery multiplication. + pub fn mul(&self, other: &Self) -> Self { + let t = montgomery::widening_mul::<6, 12>(&self.0, &other.0); + Self(Self::finish_redc(&t)) + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`montgomery::widening_square`]. Used by the exponentiation loops, not by point + /// arithmetic; see [`crate::p256::P256FieldElement::square`] for the measurement behind that + /// split. + pub fn square(&self) -> Self { + let t = montgomery::widening_square::<6, 12>(&self.0); + Self(Self::finish_redc(&t)) + } + + /// `self^-1 mod p`, or `0` if `self` is `0`. Fermat's little theorem over the public + /// exponent `p-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..6).rev() { + let limb = P_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } + + /// [`montgomery::redc`]'s `(high, extra)` result, reduced to the canonical `< p` value: if + /// `extra == 1`, `high + R_MOD_P_LIMBS` (proven to need no further reduction, the same + /// invariant every per-curve `redc` caller in this crate relies on -- verified, not checked + /// in, against 20,000 random trials plus the `(p-1)*(p-1)` worst case for this specific `p`); + /// otherwise `high`, minus `p` once more if `high >= p`. + fn finish_redc(t: &[u64; 12]) -> [u64; 6] { + let (high, extra) = montgomery::redc::<6, 12, 13>(t, &P_LIMBS, N_PRIME); + let (sum, _) = crate::nat::add(&high, &R_MOD_P_LIMBS); + let (diff, borrow) = crate::nat::sub(&high, &P_LIMBS); + let mut when_no_extra = [0u64; 6]; + ct::conditional_select( + Condition::::from_lsb(borrow), + &high, + &diff, + &mut when_no_extra, + ); + let mut result = [0u64; 6]; + ct::conditional_select( + Condition::::from_lsb(extra), + &sum, + &when_no_extra, + &mut result, + ); + result + } +} + +impl PartialEq for Bp384r1FieldElement { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. Comparing Montgomery-form limbs directly is valid: the map `x -> x*R mod p` is a + /// bijection. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..6 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for Bp384r1FieldElement {} diff --git a/crypto/ec/src/bp384r1_comb.rs b/crypto/ec/src/bp384r1_comb.rs new file mode 100644 index 000000000..5b54f38e4 --- /dev/null +++ b/crypto/ec/src/bp384r1_comb.rs @@ -0,0 +1,76 @@ +//! Fixed-base comb scalar multiplication: `[k]G` for brainpoolP384r1's base point `G`, with `k` +//! secret. Identical algorithm to [`crate::p256_comb`] -- see that module's docs for the full +//! derivation and verification methodology -- with `D * WIDTH` dividing `BITS` exactly here (`384 +//! / 6 = 64`, so `D = 64`, `FULL_COMB = 384`), unlike P-256/secp256k1/brainpoolP256r1's own +//! zero-padded case. +//! +//! Verified (not checked in) against 200 random `k` plus edge cases (`k = 1, 2, 3, n-1`), each +//! cross-checked against the standard affine group law (general `a`). + +use crate::bp384r1::Bp384r1FieldElement; +use crate::bp384r1_comb_table::{COMB_TABLE_X, COMB_TABLE_Y}; +use crate::bp384r1_point::Bp384r1JacobianPoint; +use crate::bp384r1_scalar::Bp384r1Scalar; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 6; +const BITS: usize = 384; +const D: usize = BITS.div_ceil(WIDTH); +const FULL_COMB: usize = D * WIDTH; +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]G`, branch-free in `k`. +pub fn comb_multiply_base_point(k: &Bp384r1Scalar) -> Bp384r1JacobianPoint { + let limbs = k.limbs(); + let mut r = Bp384r1JacobianPoint::INFINITY; + let top = (FULL_COMB - 1) as isize; + for i in 0..D { + let mut secret_index: usize = 0; + let mut j = top - i as isize; + while j >= 0 { + // Mutating `<` to `<=` here is an accepted mutant, not a bug: unlike every other + // curve's own comb multiplier (where `FULL_COMB > BITS` zero-pads, making the `else` + // branch reachable), this curve's `D * WIDTH == BITS` exactly (see the module docs), + // so `j` here never reaches `BITS` at all (`top = FULL_COMB - 1 = BITS - 1`) -- the + // `else` branch is unreachable, so both comparisons always select the same arm. + let bit = if (j as usize) < BITS { + (limbs[(j as usize) / 64] >> ((j as usize) % 64)) & 1 + } else { + 0 + }; + // `secret_index << 1`'s vacated low bit and `bit` (always `0` or `1`) are disjoint, so + // `|` and `^` agree here. + secret_index = (secret_index << 1) | (bit as usize); + j -= D as isize; + } + r = r.double(); + let (x, y, is_infinity) = table_lookup(secret_index); + r = r.add_affine(&x, &y, is_infinity); + } + r +} + +/// Looks up comb table entry `secret_index`, scanning every one of the [`TABLE_SIZE`] entries +/// under a mask. See [`crate::p256_comb::table_lookup`]'s docs. +fn table_lookup(secret_index: usize) -> (Bp384r1FieldElement, Bp384r1FieldElement, Condition) { + let mut x_limbs = [0u64; 6]; + let mut y_limbs = [0u64; 6]; + for i in 0..TABLE_SIZE { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + let mut next_x = [0u64; 6]; + ct::conditional_select(is_this_entry, &COMB_TABLE_X[i], &x_limbs, &mut next_x); + x_limbs = next_x; + let mut next_y = [0u64; 6]; + ct::conditional_select(is_this_entry, &COMB_TABLE_Y[i], &y_limbs, &mut next_y); + y_limbs = next_y; + } + // Entry 0 is the point at infinity; every other entry is affine (Z == 1). The accumulator's + // `add_affine` takes the identity as a flag rather than as a `Z == 0` coordinate. + let is_infinity = Condition::::is_equal(secret_index as u64, 0); + ( + Bp384r1FieldElement::from_limbs(x_limbs), + Bp384r1FieldElement::from_limbs(y_limbs), + is_infinity, + ) +} diff --git a/crypto/ec/src/bp384r1_comb_table.rs b/crypto/ec/src/bp384r1_comb_table.rs new file mode 100644 index 000000000..c72e920ad --- /dev/null +++ b/crypto/ec/src/bp384r1_comb_table.rs @@ -0,0 +1,591 @@ +//! The checked-in fixed-base comb table for `[k]G` (brainpoolP384r1's base point `G`), width 6, +//! matching this crate's other curves' choice for fields over 250 bits. See +//! [`crate::bp384r1_comb`] for the multiplier that uses this table and the algorithm it +//! implements. +//! +//! `COMB_TABLE_X[i]`/`COMB_TABLE_Y[i]` are the affine `(x, y)` coordinates of the `i`-th table +//! entry, as little-endian `u64` limbs; entry `0` is the point at infinity (`x = y = 0`, which is +//! not a valid affine coordinate pair for any point on the curve, so it's an unambiguous sentinel +//! here). +//! +//! Regenerated and compared against these checked-in values by this module's own `tests` +//! submodule below, using the same construction a (not checked in) scratch example used -- so a +//! hand-edit or transcription error in this file cannot survive `cargo test`. A unit test rather +//! than an integration test because the table is deliberately `pub(crate)`, not exposed outside +//! the crate: see [`crate::p256_comb_table`]'s docs for the same reasoning. + +pub(crate) const COMB_TABLE_X: [[u64; 6]; 64] = [ + [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ], + [ + 0xef87b2e247d4af1e, 0xe826e03436d646aa, 0xdb7fcafe0cbd10e8, 0x8847a3e77ef14fe3, + 0xa2a63a81b7c13f6b, 0x1d1c64f068cf45ff, + ], + [ + 0xdbce90c1cff67b5a, 0xa15109d050f83b66, 0x6786308c6d245def, 0x14377653cb5449d2, + 0x929fee2ea24eef97, 0x542f5fdb7f38e86a, + ], + [ + 0x1d6bd641548ad893, 0x034b2dfd22f13be2, 0x5a5273f033e35501, 0xa3527fca26303fc1, + 0x314a2ad59b176ed3, 0x2e1d0703e56bda86, + ], + [ + 0xbd4219127ede4cf0, 0xfb6868cc8ba66e80, 0xc216a9ed38a58c5d, 0x3d8f6ee85649482b, + 0x4d6dddc0370f0a15, 0x1e1ce71e1fd0110f, + ], + [ + 0x4db104322e289294, 0xbf550d7643448225, 0xfe4ede4689ffe35b, 0x27ee811a67bb225b, + 0xd02cdaae7ae254c8, 0x5fcc8b6eaa909a74, + ], + [ + 0x11ae4e3b923c900f, 0x13d0fcae9624402e, 0xaed4ac03ebea5ce4, 0xad0fc2f8b1ea0018, + 0x1cbcd25d7700f4fd, 0x2fcf74dd73039e64, + ], + [ + 0xc85b5e4b38ce17c8, 0x08627c9d610a0e60, 0xd726177f7198f025, 0x820770f03cfad383, + 0x1565b743095c4729, 0x22ecf3fa3ea7a90e, + ], + [ + 0x6bf550b04d994b04, 0x02a353bc5345a946, 0xe18a5d2d02c7f727, 0xdf4ddaccbb1fdc04, + 0x8974b5688e81af98, 0x2369dbb6397c99f1, + ], + [ + 0xe96b8c7bc4dcecf2, 0x97d9ee5ad31cbb2b, 0x9d41b2f75e305d64, 0x626441c61a0fceed, + 0x4b661582e3e118f2, 0x7ca039720424e29b, + ], + [ + 0xf57b4bab12814202, 0x5993c9ec22243468, 0x94dbf68f85e0793e, 0xf29a3297aac80aaf, + 0xd5467a9f997b3042, 0x592f6079c13ea154, + ], + [ + 0xa1887b696fafd72d, 0xc1b547a230e413af, 0xf23c1c8addc0d20f, 0xcf6d8bba4cb38cf9, + 0xf14f3c6ea8a1c76b, 0x82016f03c8974a94, + ], + [ + 0x22bd1e659415536d, 0x64fbc7fcd5609eaf, 0xaa84507650c3ae51, 0x43a458129e8107e3, + 0xb79a2f005755a48f, 0x860349cb087a89d1, + ], + [ + 0xbdb166076a427950, 0xa14c099265625345, 0x9b2df4f603c3af06, 0xc99b69a2d941cae8, + 0x7661aae18da6b24b, 0x7ef3beb6124dba11, + ], + [ + 0x34739ba22d274bad, 0xc1a4157902e1bf4a, 0xc96e13d9302e8b7e, 0x457fdf7eb669756e, + 0x37395c031556809a, 0x877d4b7e33312983, + ], + [ + 0x5760d7bb03601f94, 0x61e99571dd623c72, 0x90c581685f7a5bb0, 0xa5284298b9b5af1e, + 0x20d7b449118e290c, 0x03cabaa130d23e9b, + ], + [ + 0x0741134c03240074, 0xb78fcf8ef053b378, 0xecda8220c87b35b5, 0x7c4e4f34a327a8d1, + 0x68b242c5d5e83a85, 0x55798b8bbb576379, + ], + [ + 0x4e3849cafbfd9037, 0x118cfff0ddc6cfc3, 0xb4e9335fbd9d69d7, 0x23401cac3d821947, + 0xbe0077144602c740, 0x395c663a77f205c9, + ], + [ + 0x4537b39cbe4e22e8, 0x3dd59c12ced99b0a, 0xaf11f84b3a097d7f, 0xee4e0824f2d7f07f, + 0xead691d94287872d, 0x4aa61d005d9ef614, + ], + [ + 0xdfc80567dc28efbe, 0x0fbca04985c3780b, 0xf57ad4cfa02d3e10, 0xe2996eb360fe9336, + 0xe35eaf49e704ad62, 0x3109189ea6ed7a54, + ], + [ + 0xcb75c6d4670366c9, 0x7cba9d4146cc5b21, 0xe3ccfa7731578e10, 0x80ea978e3ecd782d, + 0x9b42f8c8fdeb6ecd, 0x56bb773fe76a5043, + ], + [ + 0x0457f1ccc2c6732a, 0x5eb682702773edb6, 0xa6eaf4cf3aacba0b, 0x5e2c080e8fb1d699, + 0x07f3a10e2f8fe317, 0x28f60a299b7cf51a, + ], + [ + 0x68f01f444e069d41, 0xe10822ffa6101371, 0xf64852aacdabcae6, 0x43afea6d7923890b, + 0xd6b5bca66855541a, 0x408eca247d71047b, + ], + [ + 0x554811720120dcc3, 0x9ff03d62f87bc41e, 0x894aa3522e3dc2e8, 0xcef8215e4653e2ce, + 0x3a324226a98fc7c2, 0x03199a4b8da0a6fb, + ], + [ + 0x10ba98863c18097b, 0xc32a4840f5164490, 0x7ce704ae62a4a4d7, 0xa48c4c964933a977, + 0x40a6146e1adc9af6, 0x3bb2d9c42f8150b8, + ], + [ + 0x0c935a6cca116def, 0x9830e42faf2696eb, 0x045c1f30c64cc135, 0x6c63eb35fce8b359, + 0x964b3650fcca1d9c, 0x312debaf5b230ee4, + ], + [ + 0x032b04fff6e21b60, 0x01ea6bf5471accf6, 0xf6eb38fe3cf3185d, 0x2d62c022d46c3145, + 0xb72c042800bdcbee, 0x69abfd5af715b98d, + ], + [ + 0xd96bc43978cd1d99, 0xe35daa2fb8c74581, 0x4bdbb224a36e8c33, 0xaf8a9bc7f1d92d69, + 0x75913740b92ce167, 0x2faf2b0323b52c81, + ], + [ + 0xc2a3d1f2693d4017, 0x8d3d63a3dc971b5e, 0xa0eeaeae7837c04c, 0x1f8ab8107f64007b, + 0x5c73f1e8548d4f8f, 0x3ded37333192674d, + ], + [ + 0x504d414b9b45addf, 0x29e934357fab5d1b, 0x874af3443478be73, 0xebf90beec4e3defb, + 0x4174b8324816865e, 0x55e381bdba7ceef2, + ], + [ + 0x8ebff5fcba1c1313, 0x1cb95a1ba6bc7111, 0x1b4a649a07fad6cb, 0xcd41794b688e00db, + 0xd963e85703b4fcc6, 0x2e164d7bea3e14b9, + ], + [ + 0x1445beb558025db8, 0xac322a4d908eefad, 0x368f330a5c759948, 0xdaebf31fd4956cc8, + 0xeb25f320914ce4c2, 0x8134206f29eb95f1, + ], + [ + 0x8fa269c78a553306, 0x01bf5e77329bac74, 0xdd8d066bf36baca8, 0xeecd692c2ff2c76d, + 0xdc00da176ad0418d, 0x3d717023b2a4ed70, + ], + [ + 0x228ba6db10550345, 0xca431dc978ae174e, 0x208d8447d449354e, 0xb318b4bcea2f95f3, + 0x893d65f589ae48d4, 0x635075d1202bf286, + ], + [ + 0x5b92e6137e5e242a, 0x10094a30fee7da2e, 0x02c4de54cf1c51cf, 0xd84e05a5999b6f5c, + 0x5ba718a3947d1805, 0x2d943ac7feeb3ea7, + ], + [ + 0xcbb04a4bdfde7d43, 0xdf85e2c1948c4eb0, 0x10ef709bb5eaf04f, 0x2cd92cf35d39c256, + 0x8087542fcc2e1c0d, 0x7784a350b5c772b0, + ], + [ + 0xf4c850c15ff897be, 0x5482c61dc4afa2df, 0xa93cc0023525181b, 0xbf2611bf7f5148fc, + 0xefd9450dd2ea8fe7, 0x17b7a3e3d5a6fc61, + ], + [ + 0x62a1110e2de6e191, 0xd183e3e632a8e274, 0x6f672bb7cde55650, 0x8ee1876d65c9ede5, + 0xa3fa0e539afd8e50, 0x01fa8423e24a4c49, + ], + [ + 0x72bf673237927cc3, 0x14a9d5ddf49bb744, 0x57c8e11c99ba5e26, 0x0e7146f4c9dd7991, + 0x4c6f13496828140c, 0x8687485bdc573053, + ], + [ + 0x243969a697f16db1, 0xec26a13bff99410b, 0xcb1b4a1280952f95, 0xcfda2daeaa51bfea, + 0x86d4493652b31c0c, 0x1a5c3ed33a1fc199, + ], + [ + 0x42991703c34edd18, 0xb72099a30b9e4f7e, 0x6a0549b1ed596731, 0x0fb3a1bd1c60ce98, + 0x1221409543a6811d, 0x5d131a037b032b08, + ], + [ + 0xedf6ca05053cec0c, 0x633c16125102cd48, 0x7cb2dd7b43b3eba8, 0x91f5e4db41905900, + 0x2f7c835a2a180ed0, 0x79632ff10da137e1, + ], + [ + 0x9043705dde010b2d, 0xba2804b36438c1e3, 0xf3dcde4dd71d8893, 0x65829075e753eda2, + 0x200636bf16a0d16f, 0x730aa91a585eb5c9, + ], + [ + 0x4eebde11d879d54d, 0xa83474da6aa4461c, 0xaefeee14f5d3bd1e, 0x94f8654371a34c19, + 0xe2d82590fa356c72, 0x4d0343a1968434bb, + ], + [ + 0x75c76717a4917753, 0x44be92951fa6038e, 0x245114b7a030e570, 0x846f4ab5f50e6c21, + 0x3b8c8ef2d93d2e79, 0x2c5a41973796de48, + ], + [ + 0x01ff5079644b2ffc, 0xf3e73b3957ed36d4, 0x52a0c0d635ea85f1, 0x02461c22cc3a89ba, + 0x5562691ba1b07a6c, 0x880d85186c8ab857, + ], + [ + 0xb4799b45153f2064, 0x7981504dc89088a5, 0xa495645a3ea21ed7, 0x054d6343cdfb7b98, + 0x23833772bd1cfbf8, 0x3ec97938bf9ffe2c, + ], + [ + 0x227f2744a50b06f8, 0x61134fcc0fac7430, 0x1d1ca54997bfb1fd, 0xf877cc0f7b68648a, + 0x96f69c844ef939bb, 0x2bf92da1e26dcfbc, + ], + [ + 0x44cb5fad40c0fac6, 0x5f0c4f1a3b4f147e, 0x6977a2e82c4f6a50, 0x60555559d375c138, + 0x30a5d4716bdf41a6, 0x68f110e6d45a333c, + ], + [ + 0xca81622a38d26672, 0xa781a3285e20c8c5, 0x45877deaf1a43120, 0x2feb636f09992c8f, + 0xa2debe061ada7673, 0x422d3039752e0929, + ], + [ + 0xd48e40b4dd48e221, 0xd049cefdc69d6f0d, 0x90996e8386454565, 0x1809b82b81b09c5c, + 0x81c646274b120122, 0x575ac42f5870ac4a, + ], + [ + 0x59644f166c2a37ca, 0xf7bc9ab490a3ce5a, 0x186e3a1d63553827, 0x2c89452299aab437, + 0xe83ea73da68c7cb7, 0x12c6e3a6dc420670, + ], + [ + 0xbfe37a90c7c42a75, 0x32c3344635133ad9, 0x5c660bf167219be8, 0x1b58799cf86079a6, + 0xf88524c61a8f1960, 0x88fb61fdd27a25c4, + ], + [ + 0x987443988d21eb29, 0xe3a6bb08d814cc88, 0x9eddc3186af298c4, 0x6204f203a09138c7, + 0x747013fbfde8af7a, 0x1200e04a988793d0, + ], + [ + 0xdf9d3a10d71b3803, 0xc256969cd5161c62, 0xebe1ea373cbc8e0b, 0xd91cb7a90d4f5007, + 0x9275ec059416bae6, 0x1804bed44f3cb49d, + ], + [ + 0x3bee2d61ef36168e, 0x23f71213a0885f45, 0x5b01074a4aadc6a9, 0x6bd5bdeb4fb174b8, + 0xbcd74f06a271f957, 0x6afb98e0484d8bff, + ], + [ + 0x8c58fd46b5d9b25c, 0xf831513f569d2a7e, 0x99e944753aed5a51, 0x60baf65ac0ca9f42, + 0x4d1a31b76f396c61, 0x85de59d8b54393c9, + ], + [ + 0x47876bbb7fc8cc5b, 0x5057387d693c21c0, 0x6d48a5533c184c52, 0x156dc4db4564c321, + 0x2d34b817bbd1cc49, 0x3d4a28a0fa71696c, + ], + [ + 0x6b923d562e6e14e9, 0x54cdf8604c0d88c0, 0x3486f0467c4bc710, 0x7ab27b82a4fe6528, + 0xc9a060ca2cdcf710, 0x0109c6381553b4ee, + ], + [ + 0xea7f72a9bbef2f55, 0x2ac3e7b912b734b5, 0x6e710c7742d91df8, 0x8d910356a75901ec, + 0x13ef3e303399916c, 0x70f166549a5ac987, + ], + [ + 0x93ddc881f814962c, 0x133ca295520a70d7, 0x59bba31fcaf99771, 0x28d380dc534900b1, + 0xb477956db3c31fcc, 0x1f01e3bf4ba1af17, + ], + [ + 0x76d668e83158eeae, 0xcad49f09b7482ed2, 0xc95d4364e75ca934, 0x6dff68ab509f589e, + 0x7d8dbf12ba2d8887, 0x7355a475b36fdfd4, + ], + [ + 0xd5e25ca4985d0af8, 0x7452d9a2108ec042, 0x087763cfd74bd2d9, 0xda8059eb21199363, + 0x2ec2dca012b45805, 0x02608b187ba04463, + ], + [ + 0x100263974a137a68, 0x64eeb98f03061ebc, 0xcedb70a9898bee68, 0x32628d70c9817b15, + 0x55be3af9f846da75, 0x0ffaeb62977a9c9f, + ], +]; + +pub(crate) const COMB_TABLE_Y: [[u64; 6]; 64] = [ + [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ], + [ + 0x42820341263c5315, 0x0e46462177918111, 0xe19c054ff9912928, 0x62b70b29feec5864, + 0x5cb1eb8e95cfd552, 0x8abe1d7520f9c2a4, + ], + [ + 0x543a4b582c0a8325, 0x06b539da86540019, 0xa9e059588394aab5, 0xca174329b65a54d8, + 0xbbc43d7c85646c64, 0x7811a0441ba94648, + ], + [ + 0x19f6a4e398af7a51, 0xd2473b4051fe7dec, 0xa4cb8fe2c4ec88fc, 0x9aeb504e2d88e230, + 0x043a7ff45e945413, 0x63f80fbde11c07cd, + ], + [ + 0x766f59437b4f2813, 0x49be01e073609367, 0xb65023f5548367ca, 0xb47aa2a5e0ae2047, + 0x9c0602b7f8767c97, 0x24c5e0596e369143, + ], + [ + 0xac87377dedac4016, 0x42048c4c80b1f898, 0xc0356930ad2c98c2, 0xc0f08c442f002e32, + 0x5ba160cdd7075816, 0x73795e05037bfbaf, + ], + [ + 0x9048d117445504a1, 0x29103c110e905f68, 0xffbb7066cf091c51, 0xbd09036e96d6cda2, + 0xe361ce28b5db4d74, 0x28a74cb3ebbb074c, + ], + [ + 0xaafa71582b2278da, 0x834fb0b36ace3000, 0xe765c223a92095cf, 0xbb44808a5b03cf55, + 0x81eaedd5f791f85c, 0x172aac2510165b40, + ], + [ + 0x0ee188bb14504c85, 0x43f143e6500cb2c1, 0xcd41cae93334ce66, 0x127368f10742fa8f, + 0x93b08775925ff0a8, 0x6f47b11daa3b124f, + ], + [ + 0x02e1628858a2512b, 0x30824ce91465d258, 0x7a1a31ae87ace1dc, 0x2bc37a851e964f85, + 0x7b089dc0f0bb86f0, 0x68ef9180090353bd, + ], + [ + 0x6342eb9b66eee47d, 0xb21242539d98b348, 0xdd77d3338ff852e1, 0x53cfbb1921561cc7, + 0x97379ed34f76fd7e, 0x471e5e9cce2e8380, + ], + [ + 0x2622d539a9121874, 0x38788d9dbdc0a747, 0xde30d0df7fd0b3a9, 0x7c1da8ec96732537, + 0xdfcfd9dc6665d191, 0x2dbee2ad377bed95, + ], + [ + 0xaf4bfde410969a85, 0x2b82da08a2362198, 0xd9247a906111cb2b, 0x8e9467abbf8fb817, + 0x946df98ae471b321, 0x6502c8760ec7ce30, + ], + [ + 0x26c9ce072292d9ca, 0xe5f732db917ca162, 0x3b70d9fb6d4b496d, 0x1d81665e4c4efb1c, + 0x58ccd9b700e1b3a5, 0x30fd2039c48b36f3, + ], + [ + 0x263a81d7ace67bc8, 0xfa7680474e276a91, 0x2b8161eed090294a, 0xe80480f78f7bbb59, + 0x1ca490df21d700d4, 0x8924c236bc1359e8, + ], + [ + 0x41e5032f44643729, 0x065f1aa2ffbc424a, 0x2aad24e004ab041d, 0xf8533e1f6717453d, + 0x03c254571bb30fed, 0x30fa9b1bc4f8d359, + ], + [ + 0x7f53b9e7bebd6493, 0x633561381757deb9, 0x15fd404b9ea99a00, 0x13f6df8a11170217, + 0xe049a0802cfa693f, 0x0d21c5552054bf26, + ], + [ + 0xb8e5a1b2de17e61e, 0x66d4f5d89dec71c7, 0xd0ba18e75e42c6aa, 0x17d7fb265a6821c5, + 0x75ec348aba5c0026, 0x3565e853af3c9cc3, + ], + [ + 0x0b1b68ec18e66047, 0xbbd4bbc24adb9058, 0xe8f3398716c7a875, 0xe138d24b664a7c50, + 0x58529d1d89a3a365, 0x7d1a84751e938267, + ], + [ + 0x9485b8dc449405e8, 0xda157550c237b714, 0xe83e91a9b20fc6d6, 0x157326a125ad81b9, + 0x138f767c61d1f1fd, 0x0b48a714ff4adb06, + ], + [ + 0xf5aa9339c44aa4a3, 0x1c422eda1459b571, 0x4c956480ede2c307, 0x268146125518634a, + 0xee70989517c9ec74, 0x695d96ff8859f3f4, + ], + [ + 0xafa315344717eebd, 0xdebba2e74888befb, 0xc10f376165dcadc5, 0x78911c3aa2ad67fa, + 0x28136e413a0c0755, 0x61dd14aaed7ebd73, + ], + [ + 0xae76105e763b5441, 0x635a4902354ef7d3, 0xd547dfb4a55bad33, 0x5068006c55876a95, + 0x4c1f391f232f1094, 0x7c66a84398032ed1, + ], + [ + 0x0c60eefb1e6dabea, 0x1c79105dc53c56b6, 0xf902ef719f41bc25, 0xfc329b88d0c436a2, + 0x2c22e639175dd49c, 0x279c4abece01267b, + ], + [ + 0x92158c384787fa39, 0xe7856c2eb7ffd20d, 0xc5ec98b236845bda, 0xaa049a7e0c4492b2, + 0x14d51c738494c5ed, 0x7a4ee1c2fc32b3e8, + ], + [ + 0xb6234a74d7b688c6, 0x554fa629bbe266ef, 0x9414a06e687e6fe9, 0x0df65babe8d4733b, + 0xade99500243ce046, 0x003ff2d0814f0d37, + ], + [ + 0x829023eb7d7dd4bd, 0xa3d879460d27a2ab, 0x33c3d5e42691b7d1, 0x599a4de400d65214, + 0x566765953c0132ed, 0x87a10c2084d089ab, + ], + [ + 0xda84a910205a9d2f, 0xcab2e91320893061, 0xa61f1803eb526eb6, 0xc7699287351c9e6c, + 0x72836cc6af88c9a1, 0x6fb69934547ad5cb, + ], + [ + 0xd26e6a119c086656, 0x30c9437ff8f3fd2e, 0xa2a6b7d784eba398, 0x832626c3de4ad701, + 0x43af1289c5aa38a1, 0x6686e32ab51a45a0, + ], + [ + 0x14d5b8da84fa6af8, 0x0b1a2e1bd58a9fb2, 0xd5e65bffe2610c5f, 0xa3921b5387c1620e, + 0x280e0c6ad1009054, 0x78a0b5443bb52e8b, + ], + [ + 0x2f5871127b4f1cf2, 0x518d43dff81ca4ac, 0xcc6db38fa525f5a3, 0xb9e37b7a3ac2682d, + 0x12e31cf33016b926, 0x471f4e421670eae0, + ], + [ + 0xad8b647e13e5152d, 0xfa9e377e180dbca7, 0x52777a2df72082e8, 0xa56fe67adba229cb, + 0xbd4fe6bc5c50c6a0, 0x30b73dc5e8d59f1e, + ], + [ + 0x3f508154cd49b977, 0x14725315bb3306f9, 0x346e09135acfba88, 0x799c0d02f296cd4e, + 0x86056b6ccd86d677, 0x876ccd183a19910e, + ], + [ + 0x2ddf7de02c5ce6ad, 0x05b6189fce3e073e, 0x6d1d42fad5f0f89a, 0x2f2b0ec9031d6c41, + 0x06e163b4a5521843, 0x546016fdc4d90ab7, + ], + [ + 0x6fb224391b1f16e7, 0x991b56ba2309e571, 0x2a8952998b85d110, 0x1a0b553bdafd75b9, + 0x246a8086ffc8ad1e, 0x715170e957f3ff5c, + ], + [ + 0xfa5d8bb4a9afd1b7, 0x5ea556c38a52f6c8, 0x83774205eaff523b, 0xb8c3c48672900829, + 0xd5c66716f1f8154d, 0x8183ec718fac8775, + ], + [ + 0xe30a43f96e28e9dd, 0xc30a9c29c4c63108, 0x69454106d72553ee, 0x63882a57e4266744, + 0x7524d0d1a19bce77, 0x6f88821c0a71b23f, + ], + [ + 0x7cf43ed18149feb4, 0x6f3eb80defe07245, 0x2c7260ce5f0f003c, 0xcd5a6e0703d8ccb7, + 0x5fca1f4e50353a27, 0x7f056ebd5588ea58, + ], + [ + 0x2eab4eeb51013a79, 0x9f71c30121cfcd10, 0x71d12400e2fe6d78, 0x47b5d2f682d53915, + 0xc6b653a242ccfa48, 0x89061bd5cd036241, + ], + [ + 0x15a7a9c82bf75173, 0x03b06641987f4e12, 0x2bc3f8cd42c01d91, 0x41cc8bcc7d9141cc, + 0xabc51856ed76ae2a, 0x1f10657fa3066aab, + ], + [ + 0xe09279097e45558e, 0x50fdcac85da18f1f, 0x4f54f4ac1f474f73, 0x12401b63d6a856c7, + 0xda5065a94045fb19, 0x389aee4a9b1ab933, + ], + [ + 0xeccf671f6ffac021, 0x899bf9f4bb453498, 0xb98f641d67fe69f5, 0x8aadc7b3d8bf39db, + 0xf639fa28f3ff938c, 0x89887a2685c3f9fa, + ], + [ + 0xb3fa384c8b94f2ac, 0x654b4c8e95cc03f4, 0xbdeb2383746fd7ec, 0x985ef24bcb97358a, + 0xed3135c3af1f87f6, 0x1eb9098e1503bcd0, + ], + [ + 0x42f259ca2f3b3bf1, 0xa39072d8742448cd, 0x3d19526f8c744283, 0x2f4c786863419e40, + 0x9fa852386bb69453, 0x686e6fa193253081, + ], + [ + 0x07e433f43701e5fd, 0x9e37d0607b767812, 0xc886816c1991b120, 0x80c99a2d4b7c4173, + 0x02770d5e84a2d7da, 0x2354178d542d162a, + ], + [ + 0x07e87f4ee950b6fd, 0xd89a114bd15c5b92, 0x9cec9c5174562596, 0x6a103acb8ab67f55, + 0xe65949e901073360, 0x446332baf22ea5c6, + ], + [ + 0xbde327227dcdbc46, 0xb0559d0597656d18, 0xe16217733f46bc8e, 0x40690e99e8bc5649, + 0x7d2a488b15eaf967, 0x4b9982565256d24d, + ], + [ + 0x5e0501315790c495, 0xe37ca498bfbb1ecc, 0x3f9a39cdb2a0e389, 0x52b598897a603492, + 0xac5a8f043a3d208d, 0x590f9c0949b626a3, + ], + [ + 0xed4ed0cd912c4299, 0x434f13ed1ebf188c, 0xc67d4cdae448825e, 0x7a9eb2b82356d96e, + 0x4bd2e317b493d442, 0x2457344a424caa5c, + ], + [ + 0x318cc3175ac19b41, 0xd460ab3d4d948d58, 0x629dae430f1efdff, 0x41b7aa1b20f3f28e, + 0x71e33b90f4a45b9d, 0x010b0965bd7278f7, + ], + [ + 0x3fea803384f5ca88, 0xd11f71a1cf81ea8e, 0x85fff84c5d4e454d, 0x5993450a81908a50, + 0x380b424c2ca3f02b, 0x48a23ad7bc1adb2b, + ], + [ + 0xeb7c31475d3d8c55, 0x4104137e46ee8546, 0xf6f5742e438b3c9e, 0x26030829078e88ba, + 0xb543e808eb899bea, 0x186f73d9fd670717, + ], + [ + 0x016dab4ea28f2df0, 0x2796b012efc71244, 0xedaff0e9f860e3a9, 0xe793f8b750207d08, + 0xaf338c9ca92e6bc7, 0x03bfd6bdc6db9d7c, + ], + [ + 0x3f032da29c692e97, 0xee2a85c1b9f3fe79, 0xb741bfcdb186fdce, 0xf1cbfef3219ad8a0, + 0x84a2a9b760047895, 0x8bbd51542a661ee8, + ], + [ + 0x2f84f1f40f034308, 0x35f19d80414a5a69, 0x758a59ddbc4ec094, 0xef1377feca60f859, + 0xbf189ff64132d431, 0x77ae2f7db656e6f1, + ], + [ + 0x3aac520da310bac4, 0x303a07e636e0d011, 0xdd4b2249aaf0807e, 0x4024cd891cabc7ff, + 0x4996456db3fc2a82, 0x19b4c43f52dbe463, + ], + [ + 0xb4f726cd1ab0f453, 0xd1c91535c7e3e798, 0xe96dcf5d5eea6143, 0x821a99455815032e, + 0x3a54ecd8023ed4e8, 0x7611baf1b3a9cd0a, + ], + [ + 0xdb388dea399ee8ed, 0xb7ee1a80cdbb9ccc, 0x321122d9bf45a0bc, 0xf5699fc0d95c7cda, + 0xd5b9354e79a68abc, 0x1028b8a13e9a8bcc, + ], + [ + 0x5815ad5c79adc16b, 0xb6fb56de6c1ecd9b, 0xa85a4518d8ab38b5, 0x0c8389d6c4544048, + 0x48b2665e184bb945, 0x70bec8ec1118331b, + ], + [ + 0x30357d716eb72c64, 0x2f63992dd1ef0d1a, 0xaec2c6c92daf31e4, 0xf3acc45480dfc019, + 0xc831f79684a06be3, 0x6a7545417a7ce293, + ], + [ + 0x542d9e1e9c71f5ca, 0x0263d99828f20d37, 0xa78c69447157a541, 0xd058bf0840f3b157, + 0x25d6c1fbb2412b14, 0x359178ba867312de, + ], + [ + 0x6f3713b73992176d, 0x321b17caa771435e, 0x83daa0ef78b0b9e7, 0x1fb6a2850ff2849a, + 0x3c4cb9456e2e6572, 0x223696772b8c6afe, + ], + [ + 0xf92e19d5da0e2196, 0xd953b0814573801c, 0xc7a1d1a292c579fa, 0xcc87acff091cb84d, + 0x740476bd7800ee3b, 0x58489336c25345a2, + ], + [ + 0x659e27463c3d03b2, 0x40e9af25d51c08a4, 0x1f7c8af459dc69fe, 0xe34e0c21997a9afc, + 0x1082aaac5f85cef5, 0x3f53f37690695783, + ], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::bp384r1::Bp384r1FieldElement; + use crate::bp384r1_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::bp384r1_point::Bp384r1JacobianPoint; + + const WIDTH: usize = 6; + const BITS: usize = 384; + const D: usize = BITS.div_ceil(WIDTH); + const TABLE_SIZE: usize = 1 << WIDTH; + + /// Regenerates the comb table from scratch via the crate's own (independently verified) + /// point arithmetic, per this module's construction: `pow2[i] = 2^(i*D) * G`, then + /// `table[idx] = sum of pow2[b] for each bit b set in idx`. + #[test] + fn regenerated_table_matches_checked_in_constants() { + let g = Bp384r1JacobianPoint::from_affine( + Bp384r1FieldElement::from_limbs(G_X_LIMBS), + Bp384r1FieldElement::from_limbs(G_Y_LIMBS), + ); + + let mut pow2 = [g; WIDTH]; + for i in 1..WIDTH { + let mut p = pow2[i - 1]; + for _ in 0..D { + p = p.double(); + } + pow2[i] = p; + } + + let mut table = [Bp384r1JacobianPoint::INFINITY; TABLE_SIZE]; + for bit in (0..WIDTH).rev() { + let step = 1usize << bit; + let pw = pow2[bit]; + let mut i = step; + while i < TABLE_SIZE { + table[i] = table[i - step].add(&pw); + i += 2 * step; + } + } + + for (idx, point) in table.iter().enumerate() { + if idx == 0 { + assert!(point.is_infinity().to_bool(), "table[0] must be infinity"); + assert_eq!(COMB_TABLE_X[0], [0, 0, 0, 0, 0, 0]); + assert_eq!(COMB_TABLE_Y[0], [0, 0, 0, 0, 0, 0]); + } else { + let (x, y) = point + .to_affine() + .unwrap_or_else(|| panic!("regenerated table[{idx}] is unexpectedly infinity")); + assert_eq!(x.to_limbs(), COMB_TABLE_X[idx], "table[{idx}].x mismatch"); + assert_eq!(y.to_limbs(), COMB_TABLE_Y[idx], "table[{idx}].y mismatch"); + } + } + } +} diff --git a/crypto/ec/src/bp384r1_domain.rs b/crypto/ec/src/bp384r1_domain.rs new file mode 100644 index 000000000..51f6da6a2 --- /dev/null +++ b/crypto/ec/src/bp384r1_domain.rs @@ -0,0 +1,36 @@ +//! brainpoolP384r1 domain parameters, RFC 5639 §3.4. `p` lives in [`crate::bp384r1`]; `n` lives in +//! [`crate::bp384r1_scalar`]. This module holds the rest: the curve coefficients `A` and `B` and +//! the base point `G`. +//! +//! Unlike every NIST curve or secp256k1 in this crate, brainpool's `A` is neither `-3` nor `0`, so +//! it has no dedicated doubling-formula shortcut: [`crate::bp384r1_point`] uses the general-`a` +//! "dbl-2007-bl" formula instead. +//! +//! Every value below was extracted directly from a freshly downloaded copy of RFC 5639 §3.4's text +//! (concatenating the line-wrapped hex under `Curve-ID: brainpoolP384r1`, not retyped) and verified +//! in Python before any of this was ported to Rust: `y^2 = x^3 + Ax + B (mod p)` holds for `(G_X, +//! G_Y)`. + +/// The curve coefficient `A` in `y^2 = x^3 + Ax + B`, little-endian `u64` limbs. +pub const A_LIMBS: [u64; 6] = [ + 0x04a8c7dd22ce2826, 0x8aa5814a503ad4eb, 0x139165efba91f90f, 0xc2bea28e4fb22787, + 0x3c72080ace05afa0, 0x7bc382c63d8c150c, +]; + +/// The curve coefficient `B` in `y^2 = x^3 + Ax + B`, little-endian `u64` limbs. +pub const B_LIMBS: [u64; 6] = [ + 0x3ab78696fa504c11, 0x7cb4390295dbc994, 0x2e880ea53eeb62d5, 0x2fb77de107dcd2a6, + 0x8b39b55416f0447c, 0x04a8c7dd22ce2826, +]; + +/// The base point `G`'s `x` coordinate, little-endian `u64` limbs. +pub const G_X_LIMBS: [u64; 6] = [ + 0xef87b2e247d4af1e, 0xe826e03436d646aa, 0xdb7fcafe0cbd10e8, 0x8847a3e77ef14fe3, + 0xa2a63a81b7c13f6b, 0x1d1c64f068cf45ff, +]; + +/// The base point `G`'s `y` coordinate, little-endian `u64` limbs. +pub const G_Y_LIMBS: [u64; 6] = [ + 0x42820341263c5315, 0x0e46462177918111, 0xe19c054ff9912928, 0x62b70b29feec5864, + 0x5cb1eb8e95cfd552, 0x8abe1d7520f9c2a4, +]; diff --git a/crypto/ec/src/bp384r1_extra_bits.rs b/crypto/ec/src/bp384r1_extra_bits.rs new file mode 100644 index 000000000..fb1bbdb70 --- /dev/null +++ b/crypto/ec/src/bp384r1_extra_bits.rs @@ -0,0 +1,57 @@ +//! FIPS 186-5 Appendix A.4.1, "Conversion of a Bit String to an Integer mod n via Modular +//! Reduction" -- the "extra random bits" method both `bouncycastle-ecdsa`'s `keys_bp384r1`'s key generation +//! (Appendix A.2.1) and `bouncycastle-ecdsa`'s `ecdsa_bp384r1`'s randomised per-message secret (Appendix A.3.1) +//! use to turn a DRBG output wider than `n` into a value in `[1, n-1]` with negligible bias. +//! Identical algorithm to [`crate::p256_extra_bits`] -- see that module's docs for the full reasoning +//! -- with brainpoolP384r1's own `n` substituted. +//! +//! # Why brainpoolP384r1 needs this at all +//! +//! Unlike P-384/P-521/secp256k1 (whose `n` is so close to a power of two that Appendix A.4.1's +//! bias bound holds with no extra bits, and which draw them only because Appendix A.3.1 demands +//! `t >= 64` -- see `crate::p384_extra_bits`'s docs), brainpoolP384r1's `n` is not +//! close to a power of two: `2^384 - n` is about a third of `2^384`, not a small remainder -- +//! identical in shape to brainpoolP256r1's own case, just at this curve's own width. FIPS 186-5 +//! Appendix A.4.1 step 2's bias-bound check (`2ρ(1-ρ)(n-1) > ε·N`, `ε = 2^-64`) was evaluated +//! directly (in Python, against `n`'s actual value, not assumed by analogy to any other curve) for +//! `N = 2^l` at increasing `l`: `l = 384` (matching `n`'s own bit length) fails the check, and the +//! smallest passing `l` is `445`; `bouncycastle-ecdsa` uses `l = 448` (56 bytes, byte-aligned, three bits of +//! margin above the minimum) as `bouncycastle-ecdsa`'s `keys_bp384r1::EXTRA_BITS_DRBG_OUTPUT_LEN`. +//! +//! The reduction itself is [`crate::barrett`]'s constant-time Barrett reduction with +//! `m = n - 1` (see [`crate::p256_extra_bits`]'s docs for the constant-time argument); `bp384r1_extra_bits_tests.rs` +//! pins known answers computed in Python and cross-checks it against the bit-serial algorithm this +//! module used to implement, kept there as an independent reference. + +use crate::barrett; +use crate::bp384r1_scalar::{Bp384r1Scalar, N_LIMBS}; +use crate::nat; + +/// `n - 1`, little-endian `u64` limbs. `N_LIMBS[0]` is odd (`n` is prime), so the subtraction +/// never borrows out of the low limb. +const N_MINUS_1_LIMBS: [u64; 6] = + [N_LIMBS[0] - 1, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3], N_LIMBS[4], N_LIMBS[5]]; + +/// `floor(2^768 / (n-1)) - 2^384`: the low limbs of Barrett's `mu` for `m = n - 1` (whose top +/// limb is `mu`'s only other bit -- see [`crate::barrett`]), computed in Python from +/// RFC 5639 §3.4's `n`. +const MU_LOW_LIMBS: [u64; 6] = [ + 0x600adcccf8a71f8d, 0x189fdb467a652109, 0xc506f2fe165031e7, 0xdda2c449cae56ee1, + 0xff25adfd3cc6fa65, 0xd1b575b16d8ec6b8, +]; + +/// FIPS 186-5 Appendix A.4.1 steps 3-5 for `n` = the curve order: reduces the big-endian bit string +/// `bytes` modulo `n-1` and adds `1`, landing in `[1, n-1]`. `bytes` may be up to `95` bytes +/// -- `2^760 < (n-1) * 2^384`, Barrett's precondition -- which covers the 56-byte DRBG +/// output `bouncycastle-ecdsa` draws with room to spare; a longer input is a programming error and panics. +pub fn reduce_wide_bits_mod_n_minus_1(bytes: &[u8]) -> Bp384r1Scalar { + assert!( + bytes.len() < 96, + "reduce_wide_bits_mod_n_minus_1 given {} bytes, more than 95", + bytes.len() + ); + let t = barrett::limbs_from_be_bytes::<12>(bytes); + let reduced = barrett::reduce::<6, 12, 7>(&t, &N_MINUS_1_LIMBS, &MU_LOW_LIMBS); + let (plus_one, _) = nat::add(&reduced, &[1, 0, 0, 0, 0, 0]); + Bp384r1Scalar::from_limbs(plus_one) +} diff --git a/crypto/ec/src/bp384r1_mul.rs b/crypto/ec/src/bp384r1_mul.rs new file mode 100644 index 000000000..be157177e --- /dev/null +++ b/crypto/ec/src/bp384r1_mul.rs @@ -0,0 +1,100 @@ +//! Constant-time variable-base scalar multiplication: `[k]Q` for a secret `k` and a point `Q` +//! that is not the base point -- a peer's public key, for the ECC CDH primitive of SP 800-56A +//! Rev. 3 Section 5.7.1.2 (`P = h d_A Q_B`). +//! +//! The crate's other two multipliers do not cover this case: [`crate::bp384r1_comb`] is fixed-base +//! (`[k]G` from a compile-time table of multiples of `G`), and [`crate::bp384r1_wnaf`] takes +//! [`crate::bp384r1_scalar::Bp384r1PublicScalar`] and is variable-time by design. This one takes +//! [`Bp384r1Scalar`], the secret type, and is branch-free in `k` throughout. +//! +//! # Fixed window, width 4 +//! +//! `[k]Q` is computed left to right over `ROUNDS = ceil(BITS / 4)` windows of 4 bits: each round +//! doubles the accumulator four times and adds `[w]Q` for the round's window value `w`, read off +//! `k`'s limbs at positions fixed by the loop (so *which* bits are read is public, only their +//! values are secret). `[w]Q` comes from a table of `[0]Q .. [15]Q` built per call in Jacobian +//! coordinates -- entry 0 is the identity, the rest by doubling and adding `Q` -- and is read by +//! `table_lookup`, which scans every entry under a mask rather than indexing by `w`, as the +//! comb's lookup does. The accumulator's addition is the complete [`Bp384r1JacobianPoint::add`]: it +//! masks in the identity, same-point and opposite-point cases, so the accumulator may be the +//! identity (it starts there and stays there through leading zero windows), a table entry may be +//! the identity (`w = 0`), and `Q` itself may be added to a multiple of `Q`; the comb's "no +//! offset" argument carries over unchanged. +//! +//! Cost: `4 * ROUNDS` doublings, `ROUNDS` full additions, `ROUNDS` masked scans of the table and +//! 14 point operations to build it: on P-256 about three times the fixed-base comb (whose table +//! is precomputed and whose additions are mixed) and 1.3 times the variable-time wNAF multiplier +//! (136 us against 44 us and 106 us in `p256_point_benches` / `p256_scalar_benches`). The table +//! is `16 * size_of::()` bytes of stack. Width 4 over width 5 keeps +//! that table at 1.5 KiB here and 3.4 KiB on P-521 for one fewer addition in five; a Montgomery +//! ladder would need an addition per bit. The table and the working accumulator are scrubbed +//! before returning; the caller owns the result and scrubs it when it is done. +//! +//! Verified against [`crate::bp384r1_comb`] (for `Q = G`), against [`crate::bp384r1_wnaf`] and against +//! `[a k mod n]G` (for `Q = [a]G`) in `bp384r1_mul_tests.rs`, including `k = 0`, `k = 1`, `k = n-1`, +//! `Q` the identity, `Q` negated and `Q` in a non-canonical-`Z` representation. + +use crate::bp384r1_point::{Bp384r1JacobianPoint, select_point}; +use crate::bp384r1_scalar::Bp384r1Scalar; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 4; +const BITS: usize = 384; +const ROUNDS: usize = BITS.div_ceil(WIDTH); +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]Q`, branch-free in `k`. `Q` may be any point, including the identity; `k` may be any +/// value in `[0, n-1]`, and `[0]Q` is the identity. +pub fn multiply_point(k: &Bp384r1Scalar, q: &Bp384r1JacobianPoint) -> Bp384r1JacobianPoint { + let mut table = [Bp384r1JacobianPoint::INFINITY; TABLE_SIZE]; + table[1] = *q; + for i in 2..TABLE_SIZE { + // Which entry is built how depends only on the (public) index, never on `k` or `Q`. + table[i] = if i % 2 == 0 { table[i / 2].double() } else { table[i - 1].add(q) }; + } + + let limbs = k.limbs(); + let mut r = Bp384r1JacobianPoint::INFINITY; + for round in 0..ROUNDS { + let low_bit = (ROUNDS - 1 - round) * WIDTH; + let window = window_at(limbs, low_bit); + for _ in 0..WIDTH { + r = r.double(); + } + let mut entry = table_lookup(&table, window); + r = r.add(&entry); + entry.zeroize(); + } + + for entry in table.iter_mut() { + entry.zeroize(); + } + r +} + +/// The `WIDTH` bits of `limbs` from bit `low_bit` up, as a window value in `0..TABLE_SIZE`. Bits +/// at or above `BITS` read as zero, so on a curve whose `BITS` is not a multiple of `WIDTH` +/// (P-521) the top window sees only the scalar. The bit positions are public; the shifts and +/// masks are branch-free in the limb values. +fn window_at(limbs: &[u64; 6], low_bit: usize) -> usize { + let mut window = 0usize; + for j in (low_bit..low_bit + WIDTH).rev() { + let bit = if j < BITS { (limbs[j / 64] >> (j % 64)) & 1 } else { 0 }; + window = (window << 1) | (bit as usize); + } + window +} + +/// Table entry `secret_index`, selected by scanning every entry under a mask so the index never +/// steers which memory is read. +fn table_lookup( + table: &[Bp384r1JacobianPoint; TABLE_SIZE], + secret_index: usize, +) -> Bp384r1JacobianPoint { + let mut r = Bp384r1JacobianPoint::INFINITY; + for (i, entry) in table.iter().enumerate() { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + r = select_point(is_this_entry, entry, &r); + } + r +} diff --git a/crypto/ec/src/bp384r1_point.rs b/crypto/ec/src/bp384r1_point.rs new file mode 100644 index 000000000..5a08c1488 --- /dev/null +++ b/crypto/ec/src/bp384r1_point.rs @@ -0,0 +1,399 @@ +//! Branch-free Jacobian-coordinate point arithmetic on the brainpoolP384r1 curve `y^2 = x^3 + Ax + +//! B` (RFC 5639 §3.4). Unlike every NIST curve (`a = -3`) or secp256k1 (`a = 0`) in this crate, +//! brainpool's `A` is a "random" value with neither shortcut available. +//! +//! [`Bp384r1JacobianPoint::add`]'s exceptional-case handling (infinity, same-point, opposite-point) +//! is identical to [`crate::p256_point::P256JacobianPoint::add`] -- see that module's docs for the +//! full derivation and verification methodology, which applies unchanged here, since `generic_add` +//! doesn't depend on `a`. Only [`Bp384r1JacobianPoint::double`] differs: this is the general-`a` +//! doubling formula ("dbl-2007-bl" in the hyperelliptic.org explicit-formulas database): `XX = +//! X1^2`, `YY = Y1^2`, `YYYY = YY^2`, `ZZ = Z1^2`, `S = 2*((X1+YY)^2-XX-YYYY)`, `M = 3*XX + +//! a*ZZ^2`, `T = M^2 - 2*S`, `X3 = T`, `Y3 = M*(S-T) - 8*YYYY`, `Z3 = (Y1+Z1)^2 - YY - ZZ`, +//! verified (not checked in) against the affine doubling law over 500 random on-curve points, +//! using brainpoolP384r1's actual `A`, before being ported here. + +use crate::bp384r1::Bp384r1FieldElement; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// The curve coefficient `A` in the field's Montgomery representation (`A * 2^384 mod p`), +/// computed in Python from RFC 5639 §3.4's `A` and pinned against +/// [`Bp384r1FieldElement::from_limbs`] by this module's tests. [`Bp384r1JacobianPoint::double`] +/// needs `A` on every call; converting the plain `A_LIMBS` into Montgomery form each time cost a +/// full field multiplication per doubling. +const A_MONTGOMERY_LIMBS: [u64; 6] = [ + 0xdb26b895466c3c99, 0x75d7f3fef157b07b, 0x936771b9d7f10db4, 0xe7ffe9e535529374, + 0x400a8fdf42b00c60, 0x7c338021a2e8c0d1, +]; + +/// A point on the brainpoolP384r1 curve in Jacobian coordinates. +#[derive(Clone, Copy, Debug)] +pub struct Bp384r1JacobianPoint { + /// `X`, the Jacobian `X` coordinate. + pub x: Bp384r1FieldElement, + /// `Y`, the Jacobian `Y` coordinate. + pub y: Bp384r1FieldElement, + /// `Z`, the Jacobian `Z` coordinate; `Z == 0` denotes the point at infinity. + pub z: Bp384r1FieldElement, +} + +impl Bp384r1JacobianPoint { + /// The point at infinity, the identity of the curve's group. + pub const INFINITY: Self = Self { + x: Bp384r1FieldElement::ONE, + y: Bp384r1FieldElement::ONE, + z: Bp384r1FieldElement::ZERO, + }; + + /// Builds a Jacobian point from affine coordinates (`Z = 1`). Does not check that `(x, y)` is + /// actually on the curve -- see [`crate::bp384r1_sec1`] for that. + pub fn from_affine(x: Bp384r1FieldElement, y: Bp384r1FieldElement) -> Self { + Self { x, y, z: Bp384r1FieldElement::ONE } + } + + /// TRUE iff this is the point at infinity. + pub fn is_infinity(&self) -> Condition { + self.z.is_zero() + } + + /// Converts to affine `(x, y)` coordinates, or `None` for the point at infinity. Not constant + /// time: see [`crate::p256_point::P256JacobianPoint::to_affine`]'s docs. + pub fn to_affine(&self) -> Option<(Bp384r1FieldElement, Bp384r1FieldElement)> { + if self.is_infinity().to_bool() { + return None; + } + let z_inv = self.z.invert(); + let z_inv2 = z_inv.mul(&z_inv); + let z_inv3 = z_inv2.mul(&z_inv); + Some((self.x.mul(&z_inv2), self.y.mul(&z_inv3))) + } + + /// `-self`: negating the `Y` coordinate. + pub fn negate(&self) -> Self { + Self { x: self.x, y: self.y.negate(), z: self.z } + } + + /// Overwrites all three coordinates with zero (which is not the identity's encoding), for + /// scrubbing a secret-derived point before it goes out of scope; see + /// [`Bp384r1FieldElement::zeroize`]. + pub fn zeroize(&mut self) { + self.x.zeroize(); + self.y.zeroize(); + self.z.zeroize(); + } + + /// `2 * self`, via the general-`a` doubling formula ("dbl-2007-bl") described in the module + /// docs. + pub fn double(&self) -> Self { + let a = Bp384r1FieldElement::from_internal_limbs(A_MONTGOMERY_LIMBS); + let xx = self.x.mul(&self.x); // XX = X1^2 + let yy = self.y.mul(&self.y); // YY = Y1^2 + let yyyy = yy.mul(&yy); // YYYY = YY^2 + let zz = self.z.mul(&self.z); // ZZ = Z1^2 + let x1_plus_yy = self.x.add(&yy); + let s = x1_plus_yy.mul(&x1_plus_yy).sub(&xx).sub(&yyyy); + let s = s.add(&s); // S = 2*((X1+YY)^2 - XX - YYYY) + let m = xx.add(&xx).add(&xx).add(&a.mul(&zz.mul(&zz))); // M = 3*XX + a*ZZ^2 + let t = m.mul(&m).sub(&s).sub(&s); // T = M^2 - 2*S + let x3 = t; + let yyyy8 = + yyyy.add(&yyyy).add(&yyyy).add(&yyyy).add(&yyyy).add(&yyyy).add(&yyyy).add(&yyyy); + let y3 = m.mul(&s.sub(&t)).sub(&yyyy8); // Y3 = M*(S-T) - 8*YYYY + let y1_plus_z1 = self.y.add(&self.z); + let z3 = y1_plus_z1.mul(&y1_plus_z1).sub(&yy).sub(&zz); // Z3 = (Y1+Z1)^2 - YY - ZZ + Self { x: x3, y: y3, z: z3 } + } + + /// The generic (`self != ±other`, neither infinite) Jacobian addition formula, plus the `H` + /// and `R` intermediates [`Self::add`] uses to detect the exceptional cases. See + /// [`crate::p256_point::P256JacobianPoint`]'s private `generic_add`'s docs -- this formula + /// doesn't depend on `a` at all, so it's identical to the NIST curves' version. + fn generic_add(&self, other: &Self) -> (Self, Bp384r1FieldElement, Bp384r1FieldElement) { + let z2_sq = other.z.mul(&other.z); + let u1 = self.x.mul(&z2_sq); + let s1 = self.y.mul(&other.z).mul(&z2_sq); + let z1_sq = self.z.mul(&self.z); + let u2 = other.x.mul(&z1_sq); + let s2 = other.y.mul(&self.z).mul(&z1_sq); + + let h = u1.sub(&u2); + let r = s1.sub(&s2); + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); + let v = h_squared.mul(&u1); + let g_neg = g.negate(); + let acc = s1.mul(&g_neg); + let g2 = g_neg.add(&v).add(&v); + let x3 = r.mul(&r).sub(&g2); + let y3_temp = v.sub(&x3); + let y3 = acc.add(&y3_temp.mul(&r)); + let z3 = h.mul(&self.z).mul(&other.z); + + (Self { x: x3, y: y3, z: z3 }, h, r) + } + + /// `self + other`, branch-free per [`crate::p256_point::P256JacobianPoint::add`]'s docs. + pub fn add(&self, other: &Self) -> Self { + let self_is_infinity = self.is_infinity(); + let other_is_infinity = other.is_infinity(); + + let (generic, h, r) = self.generic_add(other); + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + // Dropping the `!` here (making `is_opposite_point` collapse onto `is_same_point`) is an + // accepted mutant, not a bug: when `h == 0`, `generic_add`'s own `z3 = h*self.z*other.z` + // is already `0` regardless of `r`, so `generic` is already infinity-representing in the + // true opposite-point case (`h == 0, r != 0`) even without this override selecting + // `Self::INFINITY` explicitly -- see `crate::p521_point`'s identical case for the full + // argument. + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for an `other` given in **affine** coordinates (`Z2 = 1`), or the identity + /// when `other_is_infinity` is TRUE (`x2`/`y2` are then ignored). The same group law and the + /// same branch-free discipline as [`Self::add`] -- generic-add and doubling candidates computed + /// unconditionally, the exceptional cases masked in -- but with `Z2 = 1` the generic formula + /// simplifies: `U1 = X1`, `S1 = Y1` and `Z3 = H * Z1`, which drops five of its sixteen field + /// multiplications. The fixed-base comb multiplier adds a table entry to its accumulator on + /// every round, and every table entry is affine or the identity, so it is the caller this + /// exists for; `pub(crate)` because a caller has to know its point really has `Z = 1`. + pub(crate) fn add_affine( + &self, + x2: &Bp384r1FieldElement, + y2: &Bp384r1FieldElement, + other_is_infinity: Condition, + ) -> Self { + let self_is_infinity = self.is_infinity(); + + // The generic mixed addition, computed unconditionally: `U1 = X1`, `S1 = Y1`. + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2 + let r = self.y.sub(&s2); // S1 - S2 + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); // R^2 + H^3 - 2V + let y3 = acc.add(&v.sub(&x3).mul(&r)); // -S1*H^3 + (V - X3)*R + let z3 = h.mul(&self.z); // H * Z1 * Z2 with Z2 = 1 + let generic = Self { x: x3, y: y3, z: z3 }; + + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + let other = Self { x: *x2, y: *y2, z: Bp384r1FieldElement::ONE }; + + // Same priority order as `add`: the identity cases override everything else. + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, &other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for points that are **public**, branching on the exceptional cases instead + /// of computing every candidate and masking. + /// + /// [`Self::add`] must evaluate the doubling candidate on every call, because on secret inputs + /// it cannot branch on whether the doubling case applies -- that costs a full point doubling + /// (roughly a third of the addition) on every addition, whether or not it is ever used. This + /// version pays it only when the operands really are the same point, which for a scalar + /// multiplier over distinct precomputed multiples is essentially never. + /// + /// Restricted to `pub(crate)` and used only by [`crate::bp384r1_wnaf`], whose scalars are + /// [`crate::bp384r1_scalar::Bp384r1PublicScalar`] and whose points are a signer's public key + /// and the curve's own base point: every value it branches on is already known to an attacker. + /// Do not call this on anything derived from a private key or a per-message secret -- the + /// constant-time [`Self::add`] exists for that, and the scalar type split is what keeps the two + /// multipliers from being confused for one another. + pub(crate) fn add_vartime(&self, other: &Self) -> Self { + if self.is_infinity().to_bool() { + return *other; + } + if other.is_infinity().to_bool() { + return *self; + } + + let (generic, h, r) = self.generic_add(other); + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + generic + } + /// `self + (x2, y2)` for a **public**, affine `other` (`Z2 = 1`): [`Self::add_vartime`]'s + /// early-exit structure over [`Self::add_affine`]'s mixed formula, for the variable-time + /// multiplier's fixed side, whose precomputed multiples of `G` are stored affine. + /// `pub(crate)` for both of the reasons its two parents are. + pub(crate) fn add_vartime_affine( + &self, + x2: &Bp384r1FieldElement, + y2: &Bp384r1FieldElement, + ) -> Self { + if self.is_infinity().to_bool() { + return Self::from_affine(*x2, *y2); + } + + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2, with U1 = X1 + let r = self.y.sub(&s2); // S1 - S2, with S1 = Y1 + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); + let y3 = acc.add(&v.sub(&x3).mul(&r)); + let z3 = h.mul(&self.z); // H * Z1, with Z2 = 1 + Self { x: x3, y: y3, z: z3 } + } +} + +pub(crate) fn select_point( + cond: Condition, + a: &Bp384r1JacobianPoint, + b: &Bp384r1JacobianPoint, +) -> Bp384r1JacobianPoint { + Bp384r1JacobianPoint { + x: Bp384r1FieldElement::from_internal_limbs(select_limbs( + cond, + &a.x.internal_limbs(), + &b.x.internal_limbs(), + )), + y: Bp384r1FieldElement::from_internal_limbs(select_limbs( + cond, + &a.y.internal_limbs(), + &b.y.internal_limbs(), + )), + z: Bp384r1FieldElement::from_internal_limbs(select_limbs( + cond, + &a.z.internal_limbs(), + &b.z.internal_limbs(), + )), + } +} + +fn select_limbs(cond: Condition, a: &[u64; 6], b: &[u64; 6]) -> [u64; 6] { + let mut out = [0u64; 6]; + ct::conditional_select(cond, a, b, &mut out); + out +} + +// `add_vartime` is `pub(crate)` -- deliberately unreachable from outside the crate, since calling +// it on a secret point would undo the constant-time discipline [`Bp384r1JacobianPoint::add`] exists +// for -- so no integration test can reach it. Its whole contract is that it computes the same group +// law as `add`, differing only in *how* it gets there, so that is what is pinned here: agreement on +// every exceptional case, including the ones a scalar multiplier over distinct precomputed +// multiples essentially never reaches by chance (a point added to itself, and a point added to its +// own negation). QUALITY_AND_STYLE's private-function carve-out applies. +#[cfg(test)] +mod tests { + use super::*; + use crate::bp384r1_domain::A_LIMBS; + + #[test] + fn a_montgomery_constant_is_a_in_montgomery_form() { + assert_eq!( + Bp384r1FieldElement::from_internal_limbs(A_MONTGOMERY_LIMBS), + Bp384r1FieldElement::from_limbs(A_LIMBS) + ); + } + use crate::bp384r1_domain::{G_X_LIMBS, G_Y_LIMBS}; + + #[test] + fn add_affine_agrees_with_add_on_every_case() { + let g = Bp384r1JacobianPoint::from_affine( + Bp384r1FieldElement::from_limbs(G_X_LIMBS), + Bp384r1FieldElement::from_limbs(G_Y_LIMBS), + ); + // A non-canonical-Z representation of G as well: `add_affine` reads `self.z`, so `self` + // must not be assumed affine even though `other` is. + let z = Bp384r1FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = Bp384r1JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + let selves = [g, g.double(), g.negate(), Bp384r1JacobianPoint::INFINITY, scaled_g]; + let others = [g, g.double(), g.negate()]; + for a in selves { + for b in others { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_affine(&bx, &by, Condition::::FALSE).to_affine(), + a.add(&b).to_affine(), + "add_affine disagrees with add" + ); + } + // The identity flag must win regardless of the coordinates passed alongside it. + assert_eq!( + a.add_affine(&g.x, &g.y, Condition::::TRUE).to_affine(), + a.to_affine(), + "add_affine with the identity flag must return self" + ); + } + } + + #[test] + fn add_vartime_affine_agrees_with_add_on_every_case() { + let g = Bp384r1JacobianPoint::from_affine( + Bp384r1FieldElement::from_limbs(G_X_LIMBS), + Bp384r1FieldElement::from_limbs(G_Y_LIMBS), + ); + let z = Bp384r1FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = Bp384r1JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + for a in [g, g.double(), g.negate(), Bp384r1JacobianPoint::INFINITY, scaled_g] { + for b in [g, g.double(), g.negate()] { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_vartime_affine(&bx, &by).to_affine(), + a.add(&b).to_affine(), + "add_vartime_affine disagrees with add" + ); + } + } + } + + #[test] + fn add_vartime_agrees_with_add_on_every_case() { + let g = Bp384r1JacobianPoint::from_affine( + Bp384r1FieldElement::from_limbs(G_X_LIMBS), + Bp384r1FieldElement::from_limbs(G_Y_LIMBS), + ); + // G, 2G, -G and the identity cover all four of `add`'s exceptional cases pairwise, plus + // the ordinary one (e.g. G + 2G). + let points = [g, g.double(), g.negate(), Bp384r1JacobianPoint::INFINITY]; + for a in points { + for b in points { + assert_eq!( + a.add_vartime(&b).to_affine(), + a.add(&b).to_affine(), + "add_vartime disagrees with add" + ); + } + } + } +} diff --git a/crypto/ec/src/bp384r1_scalar.rs b/crypto/ec/src/bp384r1_scalar.rs new file mode 100644 index 000000000..e182264c9 --- /dev/null +++ b/crypto/ec/src/bp384r1_scalar.rs @@ -0,0 +1,280 @@ +//! The brainpoolP384r1 scalar field, arithmetic mod the curve order `n` (RFC 5639 §3.4). Like +//! [`crate::bp384r1`], `n` here has no special algebraic structure, so this is built on +//! [`crate::montgomery`]'s generic Montgomery multiplication rather than a per-curve fold -- +//! identical in shape to every other curve's own scalar field in this crate (e.g. +//! [`crate::p256_scalar`]), just delegating the widening-multiply/REDC work to the shared module +//! instead of hand-writing it again. + +use crate::montgomery; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +/// `n`, the order of the brainpoolP384r1 base point `G`, little-endian `u64` limbs (RFC 5639 +/// §3.4). +pub const N_LIMBS: [u64; 6] = [ + 0x3b883202e9046565, 0xcf3ab6af6b7fc310, 0x1f166e6cac0425a7, 0x152f7109ed5456b3, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, +]; + +/// `-n^-1 mod 2^64`, the Montgomery reduction constant. +const N_PRIME: u64 = 0x5cfedd2a5cb5bb93; + +/// `R^2 mod n` (`R = 2^384`), used to bring a plain value into Montgomery form. +const R_SQUARED_LIMBS: [u64; 6] = [ + 0xac4ed3a2de771c8e, 0x37264e202f2b6b6e, 0x2a927e3b9802688a, 0x574a74cb52d748ff, + 0x8f886dc965165fdb, 0x0ce8941a614e97c2, +]; + +/// `R mod n`, also the Montgomery representation of `1`. +const R_MOD_N_LIMBS: [u64; 6] = [ + 0xc477cdfd16fb9a9b, 0x30c5495094803cef, 0xe0e9919353fbda58, 0xead08ef612aba94c, + 0xf0a29081af19be20, 0x7346e17d5cc792d7, +]; + +/// `n - 2`, little-endian `u64` limbs -- the public exponent [`Bp384r1ScalarField::invert`] +/// raises its base to. +const N_MINUS_2_LIMBS: [u64; 6] = [ + 0x3b883202e9046563, 0xcf3ab6af6b7fc310, 0x1f166e6cac0425a7, 0x152f7109ed5456b3, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, +]; + +/// An element of the brainpoolP384r1 scalar field, i.e. an integer mod `n`. Stored internally in +/// Montgomery form; see the module docs. +#[derive(Clone, Copy, Debug)] +pub struct Bp384r1ScalarField([u64; 6]); + +impl Bp384r1ScalarField { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0, 0, 0]); + + /// The multiplicative identity. Its Montgomery form is `R mod n`, not the literal integer `1`. + pub const ONE: Self = Self(R_MOD_N_LIMBS); + + /// Builds a scalar-field element from little-endian `u64` limbs (an ordinary, non-Montgomery + /// value), reducing once if `limbs >= n` (valid since `limbs < 2^384 < 2n`), then converting + /// to Montgomery form. + pub fn from_limbs(limbs: [u64; 6]) -> Self { + let (diff, borrow) = crate::nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + let t = montgomery::widening_mul::<6, 12>(&reduced, &R_SQUARED_LIMBS); + Self(Self::finish_redc(&t)) + } + + /// Returns the canonical little-endian `u64` limbs (an ordinary, non-Montgomery value), `< n`. + pub fn to_limbs(&self) -> [u64; 6] { + let mut t = [0u64; 12]; + t[..6].copy_from_slice(&self.0); + montgomery::redc::<6, 12, 13>(&t, &N_LIMBS, N_PRIME).0 + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + crate::nat::is_zero(&self.0) + } + + /// `self + other mod n`. Operates directly on the Montgomery-form limbs: no REDC needed. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = crate::nat::add(&self.0, &other.0); + let (sum_plus_r, _) = crate::nat::add(&sum, &R_MOD_N_LIMBS); + let (diff, borrow) = crate::nat::sub(&sum, &N_LIMBS); + let mut when_no_carry = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 6]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_r, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod n`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = crate::nat::sub(&self.0, &other.0); + let (corrected, _) = crate::nat::add(&diff, &N_LIMBS); + let mut result = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod n`. + pub fn negate(&self) -> Self { + let (diff, _) = crate::nat::sub(&N_LIMBS, &self.0); + let mut result = [0u64; 6]; + ct::conditional_select(self.is_zero(), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod n`, via Montgomery multiplication. + pub fn mul(&self, other: &Self) -> Self { + let t = montgomery::widening_mul::<6, 12>(&self.0, &other.0); + Self(Self::finish_redc(&t)) + } + + /// Overwrites this value with zero, through a write the compiler may not elide. + /// + /// For the signing path, where a value of this type holds `d`, `k` or `k^-1` and would + /// otherwise stay legible on the stack after the signature is returned. This type is `Copy` + /// and carries public values too (verification's `u`, `v`, `e`, `r`, `s`), so it is not + /// wrapped in [`bouncycastle_utils::secret::Secret`] -- that would cost its `Copy`, its `const + /// ZERO`/`ONE`, and a scrub on every intermediate including the public ones. The trade is that + /// scrubbing is the caller's job, on every path out of the function; see + /// `bouncycastle_ecdsa`'s `sign_with_k` for the intended use. + /// + /// This does not reach values the compiler kept in registers or spilled itself, and is + /// defence in depth rather than a guarantee. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`montgomery::widening_square`]. Used by the exponentiation loops, not by point + /// arithmetic; see [`crate::p256::P256FieldElement::square`] for the measurement behind that + /// split. + pub fn square(&self) -> Self { + let t = montgomery::widening_square::<6, 12>(&self.0); + Self(Self::finish_redc(&t)) + } + + /// Converts a secret scalar (`d` or `k`) into Montgomery form for field arithmetic. See + /// [`crate::p256_scalar::P256ScalarField::from_secret`]'s docs. + pub fn from_secret(secret: &Bp384r1Scalar) -> Self { + Self::from_limbs(*secret.limbs()) + } + + /// `self^-1 mod n`, or `0` if `self` is `0`. Fermat's little theorem (`n` is prime) over the + /// public exponent `n-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..6).rev() { + let limb = N_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } + + /// [`montgomery::redc`]'s `(high, extra)` result, reduced to the canonical `< n` value. See + /// [`crate::bp384r1::Bp384r1FieldElement`]'s identical `finish_redc` for the invariant this + /// relies on and how it was verified for this specific `n`. + fn finish_redc(t: &[u64; 12]) -> [u64; 6] { + let (high, extra) = montgomery::redc::<6, 12, 13>(t, &N_LIMBS, N_PRIME); + let (sum, _) = crate::nat::add(&high, &R_MOD_N_LIMBS); + let (diff, borrow) = crate::nat::sub(&high, &N_LIMBS); + let mut when_no_extra = [0u64; 6]; + ct::conditional_select( + Condition::::from_lsb(borrow), + &high, + &diff, + &mut when_no_extra, + ); + let mut result = [0u64; 6]; + ct::conditional_select( + Condition::::from_lsb(extra), + &sum, + &when_no_extra, + &mut result, + ); + result + } +} + +impl PartialEq for Bp384r1ScalarField { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. Comparing Montgomery-form limbs directly is valid: the map `x -> x*R mod n` is a + /// bijection. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..6 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for Bp384r1ScalarField {} + +fn reduce_once(limbs: [u64; 6]) -> [u64; 6] { + let (diff, borrow) = crate::nat::sub(&limbs, &N_LIMBS); + let mut result = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut result); + result +} + +/// A scalar mod `n`, `< n`, held in `Secret`. See [`crate::p256_scalar::P256Scalar`]'s docs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Bp384r1Scalar(Secret<[u64; 6]>); + +impl Bp384r1Scalar { + /// Builds a secret scalar from little-endian `u64` limbs, reducing once if `limbs >= n`. + pub fn from_limbs(limbs: [u64; 6]) -> Self { + let mut secret = Secret::<[u64; 6]>::new(); + *secret = reduce_once(limbs); + Self(secret) + } + + /// Builds a secret scalar from SEC 1 §2.3.7 big-endian octets, reducing once if `>= n`. + pub fn from_be_bytes(bytes: &[u8; 48]) -> Self { + Self::from_limbs(crate::bp384r1_sec1::limbs_from_be_bytes(bytes)) + } + + /// Encodes to SEC 1 §2.3.7 big-endian octets. + pub fn to_be_bytes(&self) -> [u8; 48] { + crate::bp384r1_sec1::be_bytes_from_limbs(&self.0) + } + + /// The scalar's limbs, for the crate's own multiplier and field-arithmetic implementations to + /// read. Not exposed outside the crate. + pub(crate) fn limbs(&self) -> &[u64; 6] { + &self.0 + } +} + +/// A scalar mod `n`, `< n`, held as a plain (non-secret) value. See +/// [`crate::p256_scalar::P256PublicScalar`]'s docs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Bp384r1PublicScalar([u64; 6]); + +impl Bp384r1PublicScalar { + /// Builds a public scalar from little-endian `u64` limbs, reducing once if `limbs >= n`. + pub fn from_limbs(limbs: [u64; 6]) -> Self { + Self(reduce_once(limbs)) + } + + /// Returns the scalar's little-endian `u64` limbs, `< n`. + pub fn to_limbs(&self) -> [u64; 6] { + self.0 + } + + /// `self^-1 mod n` in **variable time**, as a [`Bp384r1ScalarField`] ready for the arithmetic + /// that follows, or `0` for `0`. For ECDSA verification's `s` (FIPS 186-5 §6.4.2 step 4), + /// which is part of the signature and so already public; nothing derived from a private key + /// or a per-message secret may reach this, which is why it lives on the public scalar type + /// and not on [`Bp384r1ScalarField`], whose `invert` is the constant-time one. See + /// [`crate::inverse_vartime`]. + pub fn invert_vartime(&self) -> Bp384r1ScalarField { + Bp384r1ScalarField::from_limbs(crate::inverse_vartime::mod_inverse(&self.0, &N_LIMBS)) + } +} diff --git a/crypto/ec/src/bp384r1_sec1.rs b/crypto/ec/src/bp384r1_sec1.rs new file mode 100644 index 000000000..306b14822 --- /dev/null +++ b/crypto/ec/src/bp384r1_sec1.rs @@ -0,0 +1,187 @@ +//! SEC 1 v2.0 (Certicom, May 2009) §2.3.3/§2.3.4/§2.3.5/§2.3.7 point encoding, decoding, and octet- +//! string encoding for brainpoolP384r1. Identical in shape to [`crate::bp256r1_sec1`] -- see that +//! module's docs for the full reasoning (SEC 1 §2.3.3/§2.3.4 cases, why there is no +//! point-at-infinity case, why partial validation is full validation here since RFC 5639 §3.4 +//! gives brainpoolP384r1 cofactor `h = 1`, and the compressed-decode square-root derivation) -- +//! with the curve equation `y^2 = x^3 + Ax + B` (a general `A`, per [`crate::bp384r1_point`], +//! instead of `a = -3`) and brainpoolP384r1's own constants (also `p ≡ 3 (mod 4)`, verified the +//! same way in this module's tests; RFC 5639 §3.4 gives both `p` and `h`). +//! +//! Field elements and scalars are encoded big-endian, fixed-width (§2.3.7's plain big-endian +//! integer encoding) -- the opposite byte order from this crate's internal little-endian `u64` +//! limbs, so [`limbs_from_be_bytes`] and [`be_bytes_from_limbs`] convert at the boundary. + +use crate::bp384r1::{Bp384r1FieldElement, P_LIMBS}; +use crate::bp384r1_domain::{A_LIMBS, B_LIMBS}; +use crate::bp384r1_point::Bp384r1JacobianPoint; +use crate::nat; + +/// `(p + 1) / 4`, the exponent [`decode`]'s compressed-point square root raises `α` to. +const SQRT_EXPONENT: [u64; 6] = [ + 0x61d1c004cc41fb15, 0xeb34e9ca6407469c, 0x04ac76865fedc448, 0xc54bdc427b5515ad, + 0x03d75bdf94399077, 0x232e47a0a8ce1b4a, +]; + +/// Converts 48 big-endian bytes to little-endian `u64` limbs. +pub fn limbs_from_be_bytes(bytes: &[u8; 48]) -> [u64; 6] { + let mut limbs = [0u64; 6]; + for i in 0..6 { + limbs[i] = u64::from_be_bytes(bytes[(40 - i * 8)..(48 - i * 8)].try_into().unwrap()); + } + limbs +} + +/// Converts little-endian `u64` limbs to 48 big-endian bytes. +pub fn be_bytes_from_limbs(limbs: &[u64; 6]) -> [u8; 48] { + let mut out = [0u8; 48]; + for i in 0..6 { + out[(40 - i * 8)..(48 - i * 8)].copy_from_slice(&limbs[i].to_be_bytes()); + } + out +} + +fn limbs_less_than_p(limbs: &[u64; 6]) -> bool { + let (_, borrow) = nat::sub(limbs, &P_LIMBS); + borrow == 1 +} + +/// `true` iff either coordinate is outside `[0, p)`. +fn either_out_of_range(x_limbs: &[u64; 6], y_limbs: &[u64; 6]) -> bool { + !limbs_less_than_p(x_limbs) || !limbs_less_than_p(y_limbs) +} + +/// `y^2 == x^3 + Ax + B`. +fn is_on_curve(x: &Bp384r1FieldElement, y: &Bp384r1FieldElement) -> bool { + let lhs = y.mul(y); + let x_cubed = x.mul(x).mul(x); + let ax = Bp384r1FieldElement::from_limbs(A_LIMBS).mul(x); + let rhs = x_cubed.add(&ax).add(&Bp384r1FieldElement::from_limbs(B_LIMBS)); + lhs == rhs +} + +/// `base^SQRT_EXPONENT mod p`. Plain (not masked) square-and-multiply: only ever runs on public +/// data (an incoming encoded point), like [`crate::p256_sec1::pow_public`]. +fn pow_public(base: &Bp384r1FieldElement, exponent_limbs: &[u64; 6]) -> Bp384r1FieldElement { + let mut result = Bp384r1FieldElement::ONE; + for limb_idx in (0..6).rev() { + let limb = exponent_limbs[limb_idx]; + for bit in (0..64).rev() { + result = result.square(); + if (limb >> bit) & 1 == 1 { + result = result.mul(base); + } + } + } + result +} + +/// SEC 1 §2.3.3 case 3: `04 || X || Y`, 97 bytes. +pub fn encode_uncompressed(x: &Bp384r1FieldElement, y: &Bp384r1FieldElement) -> [u8; 97] { + let mut out = [0u8; 97]; + out[0] = 0x04; + out[1..49].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out[49..97].copy_from_slice(&be_bytes_from_limbs(&y.to_limbs())); + out +} + +/// SEC 1 §2.3.3 case 2: `02 || X` or `03 || X`, 49 bytes, tag selected by `y mod 2`. +pub fn encode_compressed(x: &Bp384r1FieldElement, y: &Bp384r1FieldElement) -> [u8; 49] { + let mut out = [0u8; 49]; + out[0] = if y.to_limbs()[0] & 1 == 0 { 0x02 } else { 0x03 }; + out[1..49].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out +} + +/// Decodes and validates a SEC 1 §2.3.4 point encoding: 49 bytes (compressed) or 97 bytes +/// (uncompressed). See [`crate::p256_sec1::decode`]'s docs for exactly which checks this performs. +pub fn decode(bytes: &[u8]) -> Option<(Bp384r1FieldElement, Bp384r1FieldElement)> { + match bytes.len() { + 49 => { + let y_tilde: u64 = match bytes[0] { + 0x02 => 0, + 0x03 => 1, + _ => return None, + }; + let x_bytes: [u8; 48] = bytes[1..49].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + if !limbs_less_than_p(&x_limbs) { + return None; + } + let x = Bp384r1FieldElement::from_limbs(x_limbs); + let x_cubed = x.mul(&x).mul(&x); + let ax = Bp384r1FieldElement::from_limbs(A_LIMBS).mul(&x); + let alpha = x_cubed.add(&ax).add(&Bp384r1FieldElement::from_limbs(B_LIMBS)); + let beta = pow_public(&alpha, &SQRT_EXPONENT); + if beta.mul(&beta) != alpha { + return None; + } + let beta_parity = beta.to_limbs()[0] & 1; + let y = if beta_parity == y_tilde { beta } else { beta.negate() }; + Some((x, y)) + } + 97 => { + if bytes[0] != 0x04 { + return None; + } + let x_bytes: [u8; 48] = bytes[1..49].try_into().ok()?; + let y_bytes: [u8; 48] = bytes[49..97].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + let y_limbs = limbs_from_be_bytes(&y_bytes); + if either_out_of_range(&x_limbs, &y_limbs) { + return None; + } + let x = Bp384r1FieldElement::from_limbs(x_limbs); + let y = Bp384r1FieldElement::from_limbs(y_limbs); + if !is_on_curve(&x, &y) { + return None; + } + Some((x, y)) + } + _ => None, + } +} + +/// Decodes and validates an encoded public key (see [`decode`]) directly into a +/// [`Bp384r1JacobianPoint`]. +pub fn decode_point(bytes: &[u8]) -> Option { + let (x, y) = decode(bytes)?; + Some(Bp384r1JacobianPoint::from_affine(x, y)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn p_is_3_mod_4() { + assert_eq!(P_LIMBS[0] & 0b11, 3); + } + + #[test] + fn limbs_less_than_p_boundary_cases() { + assert!(limbs_less_than_p(&[0, 0, 0, 0, 0, 0])); + + let mut p_minus_1 = P_LIMBS; + p_minus_1[0] -= 1; + assert!(limbs_less_than_p(&p_minus_1)); + + assert!(!limbs_less_than_p(&P_LIMBS)); + + let mut p_plus_1 = P_LIMBS; + p_plus_1[0] += 1; + assert!(!limbs_less_than_p(&p_plus_1)); + + assert!(!limbs_less_than_p(&[u64::MAX; 6])); + } + + #[test] + fn either_out_of_range_requires_only_one_coordinate_to_be_out_of_range() { + let in_range = [0u64; 6]; + let out_of_range = P_LIMBS; + + assert!(!either_out_of_range(&in_range, &in_range)); + assert!(either_out_of_range(&out_of_range, &in_range)); + assert!(either_out_of_range(&in_range, &out_of_range)); + assert!(either_out_of_range(&out_of_range, &out_of_range)); + } +} diff --git a/crypto/ec/src/bp384r1_wnaf.rs b/crypto/ec/src/bp384r1_wnaf.rs new file mode 100644 index 000000000..4ae909aa8 --- /dev/null +++ b/crypto/ec/src/bp384r1_wnaf.rs @@ -0,0 +1,146 @@ +//! Interleaved windowed-NAF (wNAF) Shamir's-trick multiplication: `[u]G + [v]Q`, for +//! brainpoolP384r1 ECDSA verification. Identical algorithm to [`crate::p256_wnaf`] -- see that +//! module's docs for the full derivation and verification methodology -- with the limb count +//! swapped for brainpoolP384r1's (`WNAF_LEN = 385`, one more than `n`'s 384-bit length, same +//! margin as P-384's own `385 = 384 + 1`). +//! +//! `G`'s odd multiples are a compile-time affine table (`bp384r1_wnaf_table`, width 7) added by mixed +//! addition; only `Q`'s table is built per call. See [`crate::p256_wnaf`]. + +use crate::bp384r1::Bp384r1FieldElement; +use crate::bp384r1_point::Bp384r1JacobianPoint; +use crate::bp384r1_scalar::Bp384r1PublicScalar; +use crate::bp384r1_wnaf_table::{G_ODD_MULTIPLES_X, G_ODD_MULTIPLES_Y}; +use crate::nat; + +/// Window for `Q`, whose odd multiples are computed per call: 8 point additions to build the +/// table, one addition per ~6 digits to use it. +const WIDTH: usize = 5; +/// Window for `G`, whose odd multiples are a compile-time table +/// ([`G_ODD_MULTIPLES_X`]/[`G_ODD_MULTIPLES_Y`]): nothing to build, so the widest window a +/// signed `i8` digit allows, giving one addition per ~8 digits instead of ~6, and every one of +/// them a mixed addition against an affine entry. +const G_WIDTH: usize = 7; +const WNAF_LEN: usize = 385; +// Mutating this expression (e.g. `WIDTH - 2` -> `+ 2`) only grows the table `odd_multiples` builds +// and `lookup_signed` reads from; the actual digit range `compute_wnaf` can produce is bounded by +// `WIDTH` itself (unchanged), so the extra entries are simply never indexed -- an accepted mutant. +const ODD_MULTIPLE_COUNT: usize = 1 << (WIDTH - 2); + +/// `[u]G + [v]Q`. +pub fn shamir_multiply( + u: &Bp384r1PublicScalar, + v: &Bp384r1PublicScalar, + q: &Bp384r1JacobianPoint, +) -> Bp384r1JacobianPoint { + let du = compute_wnaf(u.to_limbs(), G_WIDTH); + let dv = compute_wnaf(v.to_limbs(), WIDTH); + + let table_q = odd_multiples(q); + + let mut r = Bp384r1JacobianPoint::INFINITY; + for i in (0..WNAF_LEN).rev() { + r = r.double(); + if let Some((x, y)) = lookup_signed_g(du[i]) { + r = r.add_vartime_affine(&x, &y); + } + if let Some(add_q) = lookup_signed(&table_q, dv[i]) { + r = r.add_vartime(&add_q); + } + } + r +} + +fn shr1(limbs: &[u64; 6]) -> [u64; 6] { + // Each `limbs[i] >> 1` only occupies bits 0-62 (its own bit 63 is shifted out), and + // `(limbs[i+1] & 1) << 63` only ever occupies bit 63 -- disjoint, so `|` and `^` agree here. + [ + (limbs[0] >> 1) | ((limbs[1] & 1) << 63), + (limbs[1] >> 1) | ((limbs[2] & 1) << 63), + (limbs[2] >> 1) | ((limbs[3] & 1) << 63), + (limbs[3] >> 1) | ((limbs[4] & 1) << 63), + (limbs[4] >> 1) | ((limbs[5] & 1) << 63), + limbs[5] >> 1, + ] +} + +/// The width-`width` wNAF digits of `k` (`width` at most 7, so a digit fits an `i8`), LSB-first; unused trailing entries are `0`. Not +/// constant time: only ever called on public scalars (see the module docs). +fn compute_wnaf(k_limbs: [u64; 6], width: usize) -> [i8; WNAF_LEN] { + let mut digits = [0i8; WNAF_LEN]; + let mut k = k_limbs; + let mut pos = 0; + while k != [0, 0, 0, 0, 0, 0] { + if k[0] & 1 == 1 { + // Mutating this mask (e.g. `(1 << WIDTH) - 1` -> `+ 1`) is an accepted mutant, not a + // bug: whatever mask is used, `digit` always keeps `k[0]`'s low (odd) bit, so it's + // always odd -- the only invariant `lookup_signed` and the subtract-or-add-then-`shr1` + // step below actually rely on. A degenerate mask just makes this compute a + // non-minimal (but still exactly reconstructing `k`) signed-digit sequence -- see + // `crate::p521_wnaf`'s identical case for the full argument and verification. + let mut digit = (k[0] & ((1 << width) - 1)) as i16; + if digit >= 1 << (width - 1) { + digit -= 1 << width; + } + k = if digit >= 0 { + let (new_k, borrow) = nat::sub(&k, &[digit as u64, 0, 0, 0, 0, 0]); + debug_assert_eq!(borrow, 0); + new_k + } else { + let (new_k, carry) = nat::add(&k, &[(-digit) as u64, 0, 0, 0, 0, 0]); + debug_assert_eq!(carry, 0); + new_k + }; + debug_assert!(pos < WNAF_LEN, "wNAF encoding overflowed its verified bound"); + digits[pos] = digit as i8; + } + k = shr1(&k); + pos += 1; + } + digits +} + +/// The odd multiples `1*p, 3*p, 5*p, ..., (2*ODD_MULTIPLE_COUNT - 1)*p`, indexed so entry `i` +/// holds `(2i+1)*p`. +fn odd_multiples(p: &Bp384r1JacobianPoint) -> [Bp384r1JacobianPoint; ODD_MULTIPLE_COUNT] { + let double_p = p.double(); + let mut table = [*p; ODD_MULTIPLE_COUNT]; + let mut cur = *p; + for entry in table.iter_mut() { + *entry = cur; + cur = cur.add_vartime(&double_p); + } + table +} + +/// `digit * p` where `p` is the point `table` was built from ([`odd_multiples`]), or `None` for +/// digit `0`. +fn lookup_signed( + table: &[Bp384r1JacobianPoint; ODD_MULTIPLE_COUNT], + digit: i8, +) -> Option { + // Every accepted-mutant survivor below relies on `digit` always being odd (it's built from an + // odd `k[0]` in `compute_wnaf`, see that function's docs): `digit > 0` vs `>= 0` agree once + // the `digit == 0` case is already handled above, and for odd `digit`, `(digit - 1) / 2 == + // digit / 2` (and symmetrically for `-digit`), since integer division already floors an odd + // numerator down to the same value subtracting 1 first would. + if digit == 0 { + None + } else if digit > 0 { + Some(table[(digit as usize - 1) / 2]) + } else { + Some(table[(-digit as usize - 1) / 2].negate()) + } +} + +/// `digit * G` for a width-[`G_WIDTH`] digit, read from the compile-time table of odd multiples +/// of `G` as affine coordinates (negating `y` for a negative digit), or `None` for digit `0`. +fn lookup_signed_g(digit: i8) -> Option<(Bp384r1FieldElement, Bp384r1FieldElement)> { + if digit == 0 { + return None; + } + let index = (digit.unsigned_abs() as usize - 1) / 2; + let x = Bp384r1FieldElement::from_limbs(G_ODD_MULTIPLES_X[index]); + let y = Bp384r1FieldElement::from_limbs(G_ODD_MULTIPLES_Y[index]); + Some(if digit > 0 { (x, y) } else { (x, y.negate()) }) +} diff --git a/crypto/ec/src/bp384r1_wnaf_table.rs b/crypto/ec/src/bp384r1_wnaf_table.rs new file mode 100644 index 000000000..046f38564 --- /dev/null +++ b/crypto/ec/src/bp384r1_wnaf_table.rs @@ -0,0 +1,297 @@ +//! The checked-in odd multiples of the base point `G` for [`crate::bp384r1_wnaf`]'s fixed side: +//! `G_ODD_MULTIPLES_X[i]`/`G_ODD_MULTIPLES_Y[i]` are the affine coordinates of `(2i + 1) G`, for +//! `i` in `0..32`, as little-endian `u64` limbs -- the table a width-7 wNAF over `G` indexes. +//! `Q`'s table is built at run time because `Q` is the signer's key; `G` is the curve's constant, +//! so its table need not be. +//! +//! Generated in Python from the affine group law (SP 800-186 Appendix A.1.1) and RFC 5639 §3.4's `G`, +//! independently of this crate's arithmetic, and checked there by `n * G == infinity`; this +//! module's own test regenerates every entry with the crate's point arithmetic and compares. + +/// `x` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_X: [[u64; 6]; 32] = [ + [ + 0xef87b2e247d4af1e, 0xe826e03436d646aa, 0xdb7fcafe0cbd10e8, 0x8847a3e77ef14fe3, + 0xa2a63a81b7c13f6b, 0x1d1c64f068cf45ff, + ], + [ + 0x7d7cc85b3035f11f, 0x3c11ef6596a3b889, 0x3ee1b6fcc7463bbe, 0x3df581348c6949f8, + 0x3b17452b6a27ebf5, 0x7b63205bf00ddae7, + ], + [ + 0x2cebab9fd2412dfb, 0xfbc1247651c2770d, 0x0ac96ddf237f84f4, 0x465848a4b4fbb608, + 0x5100dabea7b5f59f, 0x0d3ec4dfce264772, + ], + [ + 0x0c76dc7302bcf181, 0x44ae054e91e8aa16, 0xff47be522f7f7582, 0x747781bc8956c1e5, + 0xf7393081ddf04a64, 0x6460f955efdcbf3b, + ], + [ + 0xbe9a6fa4d102fdcc, 0x2ca04884153f3ca5, 0xcb8050cb12ff55d0, 0xa92b638fab9b5123, + 0xaba57e45a1b99b3d, 0x318ccbf708397f07, + ], + [ + 0xe1ebfa3ebd501097, 0x019ed5e75838a2e1, 0xf650cdce31a1f42a, 0x9504ee698d6ce046, + 0x8222a33a24cfcc95, 0x29d17c36e8fac6be, + ], + [ + 0x68724ba7654b8a97, 0xe866314f4cfcdecc, 0xee6435aabfce8519, 0x4f4fe2951e9b2fc9, + 0xa4ac0d1499008c7b, 0x746f20945a91d52b, + ], + [ + 0x7ecd1fd73d60e639, 0x313c2f19665476b1, 0xee00e1e96d82bb00, 0xde83d467f4a6e8e0, + 0x992302c3873505a1, 0x08d2819ad4b10108, + ], + [ + 0x4b857eab44faa702, 0x87e9b66563d3af2f, 0xca9d72edc36ce5b3, 0x388f576ac92467f7, + 0x268c0a910c4f9601, 0x4d0bfeac9bfd1564, + ], + [ + 0xc425f5660ea7b13b, 0x5dc8ae531f363c8a, 0x63112206ed0aeefa, 0x247c7f08393c0d9b, + 0x62655a825de14c70, 0x0fa18c9d90660371, + ], + [ + 0x5fa49a306129c5a8, 0x002a1f492e05a1e2, 0xe2f7ee1ab0a0f04e, 0xd1cc2f930bd09ce4, + 0xa3837062b081fa95, 0x76b1435dfe9593e9, + ], + [ + 0xde197ba7ff56e4a0, 0x6d74b82c3e3bcb4c, 0x01a3095802f41f23, 0x1ce3bc2c1bbf9809, + 0xf6848e7c1537435e, 0x512d6e4cab98bcec, + ], + [ + 0xef26e5109e68c1ad, 0xc508fef7ab757246, 0xef25a686d306e678, 0x41db4bb1e7a2d007, + 0xcf4a5a1cd62d25cc, 0x1828eed92586c516, + ], + [ + 0x350c9b20a68701d9, 0x548141489f31bd76, 0x6335948783dcc764, 0x99a1864afcc4df50, + 0x9fc89759d2da9602, 0x864075be72641d40, + ], + [ + 0xd60bc4a028f5ef5e, 0x311eaa0253480042, 0x6218cd9304e6a9fd, 0xfb78c6501abf6ee7, + 0x53608540cee9b15c, 0x4d9e97892bb92bc1, + ], + [ + 0x3cd3b35ad044b8b5, 0xb80a0e697e151608, 0xbf951564da15b170, 0xeba9f4d5aa5522da, + 0x1d703a8b1f354ad3, 0x715331344c20c113, + ], + [ + 0x49b8776ccdd681b4, 0x62625aff6ff00d7b, 0xe2cbce4635f7ef14, 0x6965f865e663a9c7, + 0x0955e043e0260433, 0x6592ec00e8448e38, + ], + [ + 0xd1b8638422ddbf53, 0x4c1dbac3644a6de3, 0xb68fd3d42aabd573, 0x813a7b714cc04826, + 0xd46123371908db0f, 0x7accbee3d9de6f7f, + ], + [ + 0xb377753962b0f6a4, 0xd2023707a506223b, 0xdf58914907a96210, 0x87891acb2256ed0f, + 0xf5330894cde848ca, 0x46bcb4c4b855c5c8, + ], + [ + 0xa30d79d4be6cb9e1, 0x131ce7e75ef22663, 0x8176426e3a39b4d4, 0xf307a79a469dc34a, + 0x20e2891bc2959318, 0x4eb2a12680dc6da8, + ], + [ + 0x4a56076ff524e52d, 0x75e39095521de54d, 0x9523333f125ab6e4, 0x5dd6186cc057c443, + 0xa96a394779313f2e, 0x1b5803cf5e30ce9a, + ], + [ + 0x9c1eb513fd3a8c88, 0xf869b3a8c2146a48, 0x920cb3d46378687c, 0x2304bddd14f91460, + 0xddb55bb66b90c290, 0x1abcd5b3dac35310, + ], + [ + 0x2fb0e74ff9e39af0, 0x3414fb037953f4e0, 0x291efcbb48050622, 0xd1cbea8558b9299c, + 0xd93978d75f699681, 0x69789931ea39499d, + ], + [ + 0xfa4648fd14a9df01, 0x0a9edfc3a00ec4b4, 0xee0777889441d1bf, 0x02fffc36323f5f41, + 0xe828f7efcc8f31ba, 0x1f2454cfedc10870, + ], + [ + 0xe5b52d1b688d8072, 0xf2150431c53cb738, 0xd9c11ab98bb65356, 0x8498238378d999c5, + 0x9363c441e05b635f, 0x0c33a611cd1aad6f, + ], + [ + 0xcade407bdce76153, 0x76390f9a86932823, 0x6629757e8294e260, 0xa98fbd9606b94567, + 0x1d57554f06d3256d, 0x629f9daacab8290d, + ], + [ + 0x5b0760e175c606d0, 0x603d557dd09b4585, 0x5709548326e1fc9b, 0x857ad401f1e9ffda, + 0x6f6b7d5d7838083a, 0x35e3fd760543127d, + ], + [ + 0xe5de463f798a94d8, 0x948e59dd9d09bef4, 0xef0d195615912f2c, 0x4d53d652db6c872d, + 0x8130e8d3a7635465, 0x6a8e60d7d7a1f129, + ], + [ + 0x5c6337fca731dc8a, 0x0745617435b19e28, 0xacd7ba39cd4c7e92, 0x6092fbc21f2baa7a, + 0x3ed42f37d00eefdf, 0x0565a2736ce6cec7, + ], + [ + 0x29e7dd7bd308a5c0, 0x3d5d429bf606b2b5, 0x871fd71f44c606f0, 0xe7718c407c50acd5, + 0xc1c4f6801061d8f4, 0x5d366e5549e4918c, + ], + [ + 0x0a71a864717ae742, 0xf2e905d2524a22fe, 0x01aff9a430ae8498, 0xd5f6bf18ce2d080d, + 0x2e2802037ccc9b53, 0x54ea3a73ea7e2e78, + ], + [ + 0x497a316cac025cb7, 0xb71a1a6b8f0c1db2, 0x7aa370615947d231, 0x5ec073a832bb299d, + 0x6947ca0f1c6b0b5b, 0x1953c08930606436, + ], +]; + +/// `y` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_Y: [[u64; 6]; 32] = [ + [ + 0x42820341263c5315, 0x0e46462177918111, 0xe19c054ff9912928, 0x62b70b29feec5864, + 0x5cb1eb8e95cfd552, 0x8abe1d7520f9c2a4, + ], + [ + 0x81d7129d48772eb3, 0x73c0edaea3b8f593, 0x7b2bd39462363e03, 0x7b2eb481ead16a5c, + 0x5521a326bc02baaf, 0x761d3a4a5f809377, + ], + [ + 0x3f9b162e47634690, 0xe059c219dfbbc32b, 0x97495c4579bb950c, 0x39f00d1d90ed0c6d, + 0xebaa167fa90635f9, 0x20168ac65e9bb101, + ], + [ + 0xfc54207824db4187, 0xed3ca9972dd90da5, 0x85f51eae54657c72, 0x5c17b17dcad568ef, + 0x33be0b515a36f3c9, 0x7a30d2af9219e43d, + ], + [ + 0x5f0c4d7c799bdd7a, 0x8a892df80129727a, 0x6519b9cb144a5af6, 0xa0a7709206e67fcd, + 0xa6f5562626e4cf07, 0x06c64153906e5540, + ], + [ + 0xa82289c636e2ab45, 0x68d387929464d8ce, 0x3f35b03f2ad19d67, 0x8687d57dfb2b569c, + 0x8c059e4d23214fb2, 0x6aaeed397a139d1e, + ], + [ + 0x83e0a265dfb80ec6, 0x7fe3e4fd4ed3b16d, 0xd7f5363e477661d1, 0x818a8375ea30532d, + 0xb613ef8f694bfb3b, 0x199ed4b68437e1af, + ], + [ + 0x36e616af3f89ae04, 0xb288a5f22a0f4bb4, 0x319433829876bbfd, 0xa9f3130ea86756e9, + 0x3c5833c41c2b396e, 0x802e50b2b2cb8001, + ], + [ + 0xc2568ca0ae0cb469, 0x52e1e0726b03b4fe, 0x9539d69bdada4475, 0x702a753f8dc81bca, + 0xe349008bcaca8e78, 0x43c90643ba2e1abc, + ], + [ + 0x4e30db44ace2b7e1, 0x5974b54ded8d1500, 0x36a74ab166c817d9, 0x7b8292d43fc931c2, + 0xb8598ad06eac656b, 0x77e37b2e05d34057, + ], + [ + 0xd02dbf4d060f1856, 0x35d7944a4b08d8d0, 0x28b6eb4b2a158679, 0xcbd279fd72ff59e9, + 0x1caf1e0133bd1851, 0x13f9e594293a1ae7, + ], + [ + 0xa34f659012e404ce, 0xdaf764151fd328de, 0xd17a69cfd8b5b382, 0x663c5cf9a5838118, + 0x384547679d43b3da, 0x58ead2105db25530, + ], + [ + 0x22616b8a1e25fa41, 0x136447f1c70e60f5, 0x6811ef5772c7bcf2, 0x28ebebc2317e4205, + 0x4d8bdf66eda511da, 0x21ffcde21119d54d, + ], + [ + 0xf92d711cf11588f8, 0x0d68626e726978de, 0xa392a608da4ca07a, 0x7bae3915529c333e, + 0xcc8c4e65b1c6728e, 0x20da57cba5fc807e, + ], + [ + 0x45d1177bd6890868, 0xfb1e072defa74b35, 0xff0ded350077ba0a, 0xcbd24ee331732c74, + 0x13197ee866275e56, 0x5bc21d5beb68cd9c, + ], + [ + 0xe8b632bd5cf75a37, 0xbe3b0a6e9e21266b, 0x905f2de6bc5d4726, 0x8d68bfbc1829c2ec, + 0x19b727c972e47e45, 0x30713bacd795b3d3, + ], + [ + 0xdfc383f3a139e49d, 0x27eb9bdf9d2d9a50, 0x42076b0c11b48068, 0x63ef0bcce201d73c, + 0x6b5dd074aa7cfc68, 0x59456d9b2168b074, + ], + [ + 0x41dae3f8c4c83463, 0x7b2532543f434c45, 0x616b42288152d3f2, 0x77f7a6051d33b75a, + 0x0b68f4a9c562646c, 0x50bb913c74071f19, + ], + [ + 0xcce5321d43af162c, 0xac2d7ec249d81b1e, 0xa47fc93768d95c53, 0x55eafe015fc4561e, + 0xb5a0af4d5c3fc466, 0x5ad7892ac34eb0c3, + ], + [ + 0xd407e76f1cc78a7a, 0x0ffffa94df79cc22, 0x169b810043f73941, 0xc935f71ee84fc015, + 0xde823aecbde36fa4, 0x24ed3f2c6ef8a0d1, + ], + [ + 0xff45d5b7ba624762, 0xe4095c2f823f3691, 0x7f8954bcadfd3608, 0x2483d04d81d48b54, + 0x433c3429a4098798, 0x34a408ddc23db969, + ], + [ + 0xfab423b0881eec42, 0x2745146681c29a41, 0x90124b5f170a8f70, 0xda84fc410f93a63d, + 0x1f9ecc36a881beb9, 0x3439891de5a0edbd, + ], + [ + 0x218eba637db7c485, 0xfb6058b133dbae72, 0x5061f7bc1333f7e9, 0x6bcc8f8f9f9b004b, + 0xd51975849e83afa7, 0x070262c15afbf4de, + ], + [ + 0xc6a35b9ecfcf2d90, 0x5937da89c065167b, 0xe40b3d966de93f71, 0x5de7e85c3cc39119, + 0x578ec623c0ed834e, 0x2e68be167b28853d, + ], + [ + 0xab9aa52dcd061f67, 0x498fdd525cad6c3e, 0xa9737984797cb78b, 0x829856974fec01da, + 0x96c5e4662fb1b4e9, 0x6b07f87f238402b1, + ], + [ + 0x0d14399ea04d8803, 0x1e4ed3c73ad35389, 0xa27e797a75d7cbf2, 0x258a859ba3daef2f, + 0x7317264bb383e607, 0x3f6945a7b6b9c622, + ], + [ + 0x2b57e4adb8b67396, 0x8c36728d85d71b6e, 0xcb2196230a39304c, 0x039b6f21836d4453, + 0xb59593a70dd01e57, 0x44baf1708afe6315, + ], + [ + 0xd0ef4fc975c215da, 0x8837531bd90f8b34, 0x172798b455b9b874, 0x6b01f605aa7a2e7a, + 0xbd8c9f68b42af78e, 0x449b2d5a9ebed76c, + ], + [ + 0x5fab2e365a322843, 0x5d69b405e257687c, 0xb6786a3e907bce1b, 0xa2d931b58d5d530f, + 0xb9c45b2f66c004f2, 0x0607bae27d0472c3, + ], + [ + 0x97b58a2d6203eaea, 0x627331345d95d1f6, 0xbefd0d3e05daa8bd, 0x49588f7ea4b26330, + 0x72d183e87baa07f3, 0x11070c4be1965861, + ], + [ + 0x9b7a5113bcbed2c8, 0x35f943977b8cbde6, 0xf557393785ddfd21, 0xa01d5589f576493c, + 0x3f42abd03078b397, 0x6f96025e27203f86, + ], + [ + 0x76b5b5b49b903e2d, 0x4d04a45969a28f81, 0xa271f2c49793102b, 0x425f51369fbdba22, + 0x8f767e9ecde7963c, 0x0c01f832eb9ad49e, + ], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::bp384r1::Bp384r1FieldElement; + use crate::bp384r1_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::bp384r1_point::Bp384r1JacobianPoint; + + #[test] + fn table_matches_the_odd_multiples_of_g_computed_by_the_point_arithmetic() { + let g = Bp384r1JacobianPoint::from_affine( + Bp384r1FieldElement::from_limbs(G_X_LIMBS), + Bp384r1FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + let mut cur = g; + for i in 0..32 { + let (x, y) = cur.to_affine().expect("an odd multiple of G below n is never infinity"); + assert_eq!(x.to_limbs(), G_ODD_MULTIPLES_X[i], "entry {i}: x of {}G", 2 * i + 1); + assert_eq!(y.to_limbs(), G_ODD_MULTIPLES_Y[i], "entry {i}: y of {}G", 2 * i + 1); + cur = cur.add_vartime(&two_g); + } + } +} diff --git a/crypto/ec/src/bp512r1.rs b/crypto/ec/src/bp512r1.rs new file mode 100644 index 000000000..1da009b77 --- /dev/null +++ b/crypto/ec/src/bp512r1.rs @@ -0,0 +1,233 @@ +//! The brainpoolP512r1 base field, GF(p) (RFC 5639 §3.4). Unlike every NIST curve or secp256k1 in +//! this crate, `p` here is a "random" prime with no Solinas-style algebraic shortcut for +//! reduction (RFC 5639 §3.1's curves are generated pseudorandomly, not chosen for a special +//! bit-pattern) -- so this field, like [`crate::bp512r1_scalar`], is built on +//! [`crate::montgomery`]'s generic Montgomery multiplication instead of a per-curve fold. +//! +//! `p`'s value and the base point's coordinates were extracted directly from RFC 5639 §3.4's text +//! (programmatically, concatenating the line-wrapped hex, not retyped) and verified in Python: `G` +//! is on the curve and `n·G` is the point at infinity, before any of this was ported to Rust. + +use crate::montgomery; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// `p`, little-endian `u64` limbs (RFC 5639 §3.4). +pub const P_LIMBS: [u64; 8] = [ + 0x28aa6056583a48f3, 0x2881ff2f2d82c685, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, +]; + +/// `-p^-1 mod 2^64`, the Montgomery reduction constant. +const N_PRIME: u64 = 0x839b32207d89efc5; + +/// `R^2 mod p` (`R = 2^512`), used to bring a plain value into Montgomery form. +const R_SQUARED_LIMBS: [u64; 8] = [ + 0x49ad144a6158f205, 0x793fb13027157905, 0x53b7f9bc905affd3, 0xe0c19a7783514a25, + 0x19486fd8d5898057, 0xa16daa5fd42bff83, 0x202e19402056eecc, 0x3c4c9d05a9ff6450, +]; + +/// `R mod p`, also the Montgomery representation of `1`. +const R_MOD_P_LIMBS: [u64; 8] = [ + 0xd7559fa9a7c5b70d, 0xd77e00d0d27d397a, 0x51325ed5195c7f19, 0x82b264ff643997bd, + 0x299c63358fccf78e, 0x34cf724c4c362df1, 0xc02b1951cc3603f8, 0x5522624724163b74, +]; + +/// `p - 2`, little-endian `u64` limbs -- the public exponent [`Bp512r1FieldElement::invert`] +/// raises its base to, per Fermat's little theorem. +const P_MINUS_2_LIMBS: [u64; 8] = [ + 0x28aa6056583a48f1, 0x2881ff2f2d82c685, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, +]; + +/// An element of the brainpoolP512r1 base field GF(p). Stored internally in Montgomery form. +#[derive(Clone, Copy, Debug)] +pub struct Bp512r1FieldElement([u64; 8]); + +impl Bp512r1FieldElement { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0, 0, 0, 0, 0]); + + /// The multiplicative identity. Its Montgomery form is `R mod p`, not the literal integer `1`. + pub const ONE: Self = Self(R_MOD_P_LIMBS); + + /// Builds a field element from little-endian `u64` limbs (an ordinary, non-Montgomery value), + /// reducing once if `limbs >= p` (valid since `limbs < 2^512 < 2p`: `p`'s top limb is + /// `0xaadd...`, comfortably more than half of `u64::MAX`, so `2p > 2^512`), then converting to + /// Montgomery form. + pub fn from_limbs(limbs: [u64; 8]) -> Self { + let (diff, borrow) = crate::nat::sub(&limbs, &P_LIMBS); + let mut reduced = [0u64; 8]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + let t = montgomery::widening_mul::<8, 16>(&reduced, &R_SQUARED_LIMBS); + Self(Self::finish_redc(&t)) + } + + /// Returns the canonical little-endian `u64` limbs (an ordinary, non-Montgomery value), `< p`. + pub fn to_limbs(&self) -> [u64; 8] { + let mut t = [0u64; 16]; + t[..8].copy_from_slice(&self.0); + montgomery::redc::<8, 16, 17>(&t, &P_LIMBS, N_PRIME).0 + } + + /// The limbs exactly as this type stores them: the Montgomery form `x*R mod p`, not the + /// integer `x`. Paired with [`Self::from_internal_limbs`] so the crate's point arithmetic can + /// select between the coordinates of two existing points without converting each one out of + /// and back into Montgomery form -- a REDC and a multiplication per coordinate, which is what + /// [`Self::to_limbs`] followed by [`Self::from_limbs`] costs. + pub(crate) fn internal_limbs(&self) -> [u64; 8] { + self.0 + } + + /// Wraps limbs already in this type's stored representation (see [`Self::internal_limbs`]), + /// skipping the reduction and conversion [`Self::from_limbs`] performs. Only for values that + /// came out of another element of this type, or a constant computed in that representation: + /// the masked selects in the point arithmetic, and the comb table's infinity sentinel. Not + /// exposed, since an out-of-range value would break every equality and reduction assumption + /// downstream; the range is checked in debug builds. + pub(crate) fn from_internal_limbs(limbs: [u64; 8]) -> Self { + debug_assert_eq!( + crate::nat::sub(&limbs, &P_LIMBS).1, + 1, + "from_internal_limbs given a value >= p" + ); + Self(limbs) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + crate::nat::is_zero(&self.0) + } + + /// Overwrites the element with zero, for scrubbing a secret-derived value before it goes out + /// of scope. Defence in depth rather than a guarantee: copies the compiler kept in registers + /// or spilled itself are not reached. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self + other mod p`. Operates directly on the Montgomery-form limbs: Montgomery form is + /// linear, so this needs no REDC, just the same carry-aware correction every Montgomery + /// scalar field in this crate uses -- `R_MOD_P_LIMBS` (`R mod p`), not `P_LIMBS`, since a + /// carry out of the top limb means the true sum is `sum + 2^512`, and `2^512 mod p` is + /// `R_MOD_P_LIMBS` by definition, not `p` itself. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = crate::nat::add(&self.0, &other.0); + let (sum_plus_r, _) = crate::nat::add(&sum, &R_MOD_P_LIMBS); + let (diff, borrow) = crate::nat::sub(&sum, &P_LIMBS); + let mut when_no_carry = [0u64; 8]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 8]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_r, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod p`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = crate::nat::sub(&self.0, &other.0); + let (corrected, _) = crate::nat::add(&diff, &P_LIMBS); + let mut result = [0u64; 8]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod p`. + pub fn negate(&self) -> Self { + let (diff, _) = crate::nat::sub(&P_LIMBS, &self.0); + let mut result = [0u64; 8]; + ct::conditional_select(self.is_zero(), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod p`, via Montgomery multiplication. + pub fn mul(&self, other: &Self) -> Self { + let t = montgomery::widening_mul::<8, 16>(&self.0, &other.0); + Self(Self::finish_redc(&t)) + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`montgomery::widening_square`]. Used by the exponentiation loops, not by point + /// arithmetic; see [`crate::p256::P256FieldElement::square`] for the measurement behind that + /// split. + pub fn square(&self) -> Self { + let t = montgomery::widening_square::<8, 16>(&self.0); + Self(Self::finish_redc(&t)) + } + + /// `self^-1 mod p`, or `0` if `self` is `0`. Fermat's little theorem over the public + /// exponent `p-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..8).rev() { + let limb = P_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } + + /// [`montgomery::redc`]'s `(high, extra)` result, reduced to the canonical `< p` value: if + /// `extra == 1`, `high + R_MOD_P_LIMBS` (proven to need no further reduction, the same + /// invariant every per-curve `redc` caller in this crate relies on -- verified, not checked + /// in, against 20,000 random trials plus the `(p-1)*(p-1)` worst case for this specific `p`); + /// otherwise `high`, minus `p` once more if `high >= p`. + fn finish_redc(t: &[u64; 16]) -> [u64; 8] { + let (high, extra) = montgomery::redc::<8, 16, 17>(t, &P_LIMBS, N_PRIME); + let (sum, _) = crate::nat::add(&high, &R_MOD_P_LIMBS); + let (diff, borrow) = crate::nat::sub(&high, &P_LIMBS); + let mut when_no_extra = [0u64; 8]; + ct::conditional_select( + Condition::::from_lsb(borrow), + &high, + &diff, + &mut when_no_extra, + ); + let mut result = [0u64; 8]; + ct::conditional_select( + Condition::::from_lsb(extra), + &sum, + &when_no_extra, + &mut result, + ); + result + } +} + +impl PartialEq for Bp512r1FieldElement { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. Comparing Montgomery-form limbs directly is valid: the map `x -> x*R mod p` is a + /// bijection. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..8 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for Bp512r1FieldElement {} diff --git a/crypto/ec/src/bp512r1_comb.rs b/crypto/ec/src/bp512r1_comb.rs new file mode 100644 index 000000000..66b387f7f --- /dev/null +++ b/crypto/ec/src/bp512r1_comb.rs @@ -0,0 +1,70 @@ +//! Fixed-base comb scalar multiplication: `[k]G` for brainpoolP512r1's base point `G`, with `k` +//! secret. Identical algorithm to [`crate::p256_comb`] -- see that module's docs for the full +//! derivation and verification methodology -- with `D * WIDTH` again not dividing `BITS` exactly +//! (`512 / 6 = 85.33`, so `D = 86`, `FULL_COMB = 516`, zero-padded), same as P-256's own case. +//! +//! Verified (not checked in) against 200 random `k` plus edge cases (`k = 1, 2, 3, n-1`), each +//! cross-checked against the standard affine group law (general `a`). + +use crate::bp512r1::Bp512r1FieldElement; +use crate::bp512r1_comb_table::{COMB_TABLE_X, COMB_TABLE_Y}; +use crate::bp512r1_point::Bp512r1JacobianPoint; +use crate::bp512r1_scalar::Bp512r1Scalar; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 6; +const BITS: usize = 512; +const D: usize = BITS.div_ceil(WIDTH); +const FULL_COMB: usize = D * WIDTH; +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]G`, branch-free in `k`. +pub fn comb_multiply_base_point(k: &Bp512r1Scalar) -> Bp512r1JacobianPoint { + let limbs = k.limbs(); + let mut r = Bp512r1JacobianPoint::INFINITY; + let top = (FULL_COMB - 1) as isize; + for i in 0..D { + let mut secret_index: usize = 0; + let mut j = top - i as isize; + while j >= 0 { + let bit = if (j as usize) < BITS { + (limbs[(j as usize) / 64] >> ((j as usize) % 64)) & 1 + } else { + 0 + }; + // `secret_index << 1`'s vacated low bit and `bit` (always `0` or `1`) are disjoint, so + // `|` and `^` agree here. + secret_index = (secret_index << 1) | (bit as usize); + j -= D as isize; + } + r = r.double(); + let (x, y, is_infinity) = table_lookup(secret_index); + r = r.add_affine(&x, &y, is_infinity); + } + r +} + +/// Looks up comb table entry `secret_index`, scanning every one of the [`TABLE_SIZE`] entries +/// under a mask. See [`crate::p256_comb::table_lookup`]'s docs. +fn table_lookup(secret_index: usize) -> (Bp512r1FieldElement, Bp512r1FieldElement, Condition) { + let mut x_limbs = [0u64; 8]; + let mut y_limbs = [0u64; 8]; + for i in 0..TABLE_SIZE { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + let mut next_x = [0u64; 8]; + ct::conditional_select(is_this_entry, &COMB_TABLE_X[i], &x_limbs, &mut next_x); + x_limbs = next_x; + let mut next_y = [0u64; 8]; + ct::conditional_select(is_this_entry, &COMB_TABLE_Y[i], &y_limbs, &mut next_y); + y_limbs = next_y; + } + // Entry 0 is the point at infinity; every other entry is affine (Z == 1). The accumulator's + // `add_affine` takes the identity as a flag rather than as a `Z == 0` coordinate. + let is_infinity = Condition::::is_equal(secret_index as u64, 0); + ( + Bp512r1FieldElement::from_limbs(x_limbs), + Bp512r1FieldElement::from_limbs(y_limbs), + is_infinity, + ) +} diff --git a/crypto/ec/src/bp512r1_comb_table.rs b/crypto/ec/src/bp512r1_comb_table.rs new file mode 100644 index 000000000..402ab0665 --- /dev/null +++ b/crypto/ec/src/bp512r1_comb_table.rs @@ -0,0 +1,591 @@ +//! The checked-in fixed-base comb table for `[k]G` (brainpoolP512r1's base point `G`), width 6, +//! matching this crate's other curves' choice for fields over 250 bits. See +//! [`crate::bp512r1_comb`] for the multiplier that uses this table and the algorithm it +//! implements. +//! +//! `COMB_TABLE_X[i]`/`COMB_TABLE_Y[i]` are the affine `(x, y)` coordinates of the `i`-th table +//! entry, as little-endian `u64` limbs; entry `0` is the point at infinity (`x = y = 0`, which is +//! not a valid affine coordinate pair for any point on the curve, so it's an unambiguous sentinel +//! here). +//! +//! Regenerated and compared against these checked-in values by this module's own `tests` +//! submodule below, using the same construction a (not checked in) scratch example used -- so a +//! hand-edit or transcription error in this file cannot survive `cargo test`. A unit test rather +//! than an integration test because the table is deliberately `pub(crate)`, not exposed outside +//! the crate: see [`crate::p256_comb_table`]'s docs for the same reasoning. + +pub(crate) const COMB_TABLE_X: [[u64; 8]; 64] = [ + [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + [ + 0x8b352209bcb9f822, 0x7c6d5047406a5e68, 0x50d1687b93b97d5f, 0xff3b1f78e2d0d48d, + 0xb43b62eef4d0098e, 0x85ed9f70b5d916c1, 0x5a21322e9c4c6a93, 0x81aee4bdd82ed964, + ], + [ + 0x718a222379fedb80, 0x5e718bba486ed3a6, 0x51ea652c9a5ca86a, 0xb0ce40b08f8cee45, + 0x9b3214ef84f947c1, 0xea23653b16e4932e, 0x19c66049496a01c7, 0x7d81895c231c6afe, + ], + [ + 0x43889d6f8c6be9ea, 0x37d156a796864fbb, 0xa60ef5a7eefaab9d, 0x3332d66d9eef40e3, + 0x698d1a571456ede3, 0x6b7571fa3a4deda4, 0xcdb5be141c76c566, 0x1c521d36e7cb5ae1, + ], + [ + 0xbcec2d9b99071f60, 0x443156e201e36ec3, 0x3859994cf9827f70, 0x0c53c0526806c675, + 0xbcdbe046f48c633a, 0xbba3700941c13193, 0xa4a156e162583419, 0x2e0e5761ae2c15dc, + ], + [ + 0x782da580cda93652, 0x9b0cff71ceab4491, 0x9332215a35582418, 0x8bae977af828a61b, + 0x0955c947fa08e784, 0xc452f5a4842eac8d, 0xbf71c8a79d0558d9, 0x6bec92c192b492b3, + ], + [ + 0x46984b0f71b15e63, 0x8ccca95b3a73b2d6, 0x0c8f5165e95b51f7, 0xae8d451aa0fe1ad2, + 0x48ecffc90b042255, 0x19798ca8b25a0c42, 0x413a0198d613ff54, 0x55af1359c95d3e54, + ], + [ + 0xfe57b0ce54d82b7e, 0x5d3c03039ef8b08a, 0xbf70f30029290e93, 0x4bfe5d000e993354, + 0xf403cf3259f22d46, 0x5bd1ef0cae72c93b, 0x51f9452fbf275ab6, 0x881e5bff2ce5bed4, + ], + [ + 0x2e87e27be62c9807, 0xbea2d0e92e6aef48, 0xaa2f9c9efcced921, 0x76f5953ca0bd6986, + 0x290c525e01abd581, 0xeacf8ff3980407ff, 0x875e9e2f61bf83ee, 0x9c91c0e5e291fa52, + ], + [ + 0x3c0eabc5875e0efe, 0xf12a4c24805ff9b9, 0x6a8d8492542115de, 0xf8b142dc74478aa8, + 0x5b0c9bc1fd30f781, 0xd5a9e3c5dfcc6c4e, 0x4054515c105967d6, 0x8e6378b06efb37a0, + ], + [ + 0x3609651b25ea5449, 0x53bf195878385f46, 0x0a60e21349538c84, 0x29978d0a9934f8f7, + 0x42eb4d7b1971823c, 0x2a3254aefe35cbef, 0x649e416aab15b1b9, 0x2a831c9dfefb2a19, + ], + [ + 0x906373ad12052537, 0xb194e757860799c7, 0x6aefa918bfa54b00, 0xaa0c2ff209c44cfa, + 0xb9916c89ea043a8c, 0x7b24eca3e73a6c7d, 0x40b9e3042126a116, 0xa2cd89c27b4b7153, + ], + [ + 0x7dfeb414d0bb060b, 0x4c38733b567f1a26, 0x03be9eb11bdc403a, 0x48f662b97be4187b, + 0xa10467531c3cf0f3, 0x2497eab7b44fb5b0, 0x2ef91d7d196e5e17, 0x9d1d208eecdcb407, + ], + [ + 0xa4c2930271b11838, 0x685ee71fe24089c9, 0xeb0cd40189ae8e50, 0x5fe4f2247058daae, + 0x4f666f4ad61dc76f, 0x3becca2c4a7e1d92, 0x4ed3e499a87f06a1, 0x4936585edf5a1d2a, + ], + [ + 0x84a4eeca0826786c, 0xac66f1779ffa6a9c, 0x44b1c81942aa4628, 0xd13ebaeb371dfa1a, + 0x6883d07dd86181a9, 0xcdbf9436a85510e1, 0x8f4709c2a09e4dac, 0x26c793ab3b55e4e7, + ], + [ + 0xd386796507ea92eb, 0xc90ac5d37513b7ff, 0x74f90c1fe1fa9e26, 0xdd09e9215ced8cb4, + 0x3936f6c418d84df4, 0xa0a469fa5cacc9c7, 0x5836717b1c948c6b, 0x5e9c1db7ce46bd7c, + ], + [ + 0x3d41c00cb2286588, 0xf21f57ddac6ebd21, 0xc14e1b86f1234198, 0xfea78e61c10bc4e9, + 0x777246f83e3f310c, 0xed25fd8b6b38426b, 0x8d6d5fd9a71f5524, 0x68c1cc8a2814c34f, + ], + [ + 0xc9eb7b8788f9de10, 0x17a63ddafadab866, 0x035532538262f0a0, 0x109c11194a32a52f, + 0x2d059075e927b316, 0x1b85017748901c63, 0x28c5d7a8849bd6c7, 0x99d74611b3cb7ae1, + ], + [ + 0xce5aa2aecad2e950, 0x31ccd860960573c8, 0xebc0271dbfdf899b, 0xb5c93008199e12cc, + 0x15fee0975950ba3e, 0xf665d82d34b0cb09, 0xcdf9140aea81d510, 0x37a7e0ad7e0ee8af, + ], + [ + 0x65770e983c9eddc0, 0x4a257487d301ab6c, 0xbd12874376e3a387, 0x5498eb66603bb154, + 0x69ca754ed7c878d2, 0xb53607e71971df07, 0xb247bf915fa8c96d, 0x01fab61896589680, + ], + [ + 0x85c2a9808dc5252b, 0xbb9290cfbdb1a9e0, 0xfcae32af9515d1f1, 0x735d3dedad27d706, + 0x6ce9db35e8e38c28, 0xa4c1b306a9dd3ccb, 0x928bdf0183a33d8e, 0x004a26804882adc7, + ], + [ + 0x836abd096300dab2, 0x17f7b705486ed40f, 0x7a27154a00d74d14, 0x2418342737aa153a, + 0x05ba881b842c203a, 0x728460a204d609ac, 0xdb47edd4089404c8, 0x42355bb93ee4f38b, + ], + [ + 0xe30320d45b69c573, 0xc72bfde4694d9b51, 0x56e5ce703ac828e3, 0x353fd7a02d94eb02, + 0x7ee58a9364caf828, 0x04f190d21334b9aa, 0xdc1164585a9b0499, 0x9e569fe08c6ce19d, + ], + [ + 0xc0b474b2d9b027ac, 0x9236649f6fb9a6ea, 0xa97b2b0540782b2e, 0xb8dbe24ab63a68b3, + 0xef45258b34d7331e, 0x7b0efb01c9a8ce89, 0xe4609f124c51f9e2, 0x4439843730bd8567, + ], + [ + 0x61b7f5d803eaf5c7, 0x1dd21753775ae8e5, 0xc38e7be7759cd365, 0xb6d5cf08eb6b182c, + 0xf48e7802d2e7ff42, 0x9992dab9a0fe3d7a, 0xf42468b7d91d9f42, 0x4247ee54390d2eae, + ], + [ + 0x1956f7009c9d0761, 0x5d660c15f987bafb, 0xf50d47b028c11f7e, 0x892b234bee27ca96, + 0xaac35d17c868b57e, 0x64ea9d20ee080217, 0xa0cf5f7dd45014a7, 0x8c14dcc6a7f8fad5, + ], + [ + 0xc3f1bfd066a0ddfb, 0xdb1c92153d03510c, 0x3785bd5d386f45dc, 0x780a234131208e17, + 0xe34e7bfc6e83a7d4, 0x89c798bb8b3cacc8, 0xa83891e20529fda0, 0x1ace5d8552af7150, + ], + [ + 0xd25363f3ccf0d620, 0x96ad0dbd46dc5a27, 0xea4a511560112f21, 0x0daa83d53893e333, + 0x0f1abbfdb1cca690, 0x591a796f54c8543b, 0x7f9d979228da4a3f, 0x3466c552c7db4bd6, + ], + [ + 0x2c7df41b07afabe1, 0xf687fab088590cdd, 0x6948b0cbf23107cd, 0x7703266941c4f997, + 0x7dd440bcb14616da, 0xfaeac7bcf3c68d35, 0xc26a143495d8a405, 0x246864f8b05ce64f, + ], + [ + 0xa82dd7f408b78bb2, 0x4908714502302bc7, 0x8c19c2f650ca3a97, 0x491d95733e9bb917, + 0xaad6d8cb485960b6, 0xf8a899ab896cb9f0, 0x99ec19064e8ba1ef, 0x02b394d5cf049570, + ], + [ + 0x1ccf73ecc8e27725, 0x6c28c540c90df9dc, 0x54730b2f03deca09, 0x414f1cd8b0271bb9, + 0x499ccab0016fe2d1, 0xe18e2b3e13b8fa42, 0x51246cfc62a44940, 0x228cacbb0d3cff9f, + ], + [ + 0xaae90ee9c141aaac, 0x490aede84b9ccf0a, 0xe49c0a460cca733d, 0x6d6344febc9ee199, + 0x5c8cc1f8ee714331, 0x1a71bd2569f04b6a, 0xeec1c1e782fe9afd, 0x8d356a975e6e5afc, + ], + [ + 0xd69c8a26d4cb9b77, 0xa15e315da57d71cb, 0xaec28beabcf73fd0, 0x6bf8aee97cb00805, + 0xb0512bdd5e128f6a, 0xfd3c2764177486b8, 0xe1e6f4b4a1725623, 0x740234794c273cb9, + ], + [ + 0xd089ed21de19c767, 0x2aa79debaea6be2f, 0x3bc357861e130e04, 0x52ecc15246304b1f, + 0x27263b963144d56e, 0x68e8397867a86877, 0x3db658f2ddb7788e, 0xa58c2cc426b33c81, + ], + [ + 0x818e4f94e5319572, 0x854c80e808d3364d, 0x31dade37628a6fa5, 0x07cf4155b6d15cf6, + 0xfc9b9deb559c1cac, 0xca880dc040500b2d, 0xdf4558724143b846, 0x1ad76a1ca4a2469e, + ], + [ + 0x814524b88018ce5c, 0xecf76406b80309f1, 0x2e12716d10b126f1, 0x80c36a1e6ec612ad, + 0x5c046bd8de0ad3e5, 0xc5c03cdbae5b8796, 0x7ebb899ac1f9f58e, 0x111332ae8b7269ed, + ], + [ + 0x44fc4ad43ec1aeef, 0xcbee9c5b6185d14c, 0xe3790ecafa176363, 0x5686d266fcf3b4cd, + 0xba454b5baedd7bfe, 0xcaddb51a3f4a2e48, 0xbd3c3e60b7613e5a, 0x425e9e23793fec55, + ], + [ + 0x355731efe8952213, 0x14477caffe53882d, 0xc48c9fe879ccce0e, 0x97738a6e77dd16db, + 0xa85ff83b219707c0, 0x9fe7f08481d2b5c6, 0xad530e6a09758fcb, 0x3f7dacc797704fe6, + ], + [ + 0x776fb580d84dba86, 0x153a581dd9b0afd8, 0x364771703cceafed, 0x8cd5ea0c637a5e39, + 0x466bf14beb8d0e7b, 0x86a27b04e98253e3, 0x0e5cdde4f8b30b42, 0x4b546aea0716cacc, + ], + [ + 0x691b037305fa6625, 0x4c027b961266a411, 0xa8d5d1986ddeb544, 0x058cf68eb844f5e2, + 0xdfe549bc722b1568, 0x808deb8fdfd7446c, 0x1d70e4b7aa886405, 0x5d65b99be9bb149c, + ], + [ + 0x0e2a706f47b580a8, 0xf8acbec34bb52ecd, 0x90dcbf2a968035f6, 0x9493ca7edc43778a, + 0x8dff00be8b4dec5a, 0xe028d793dcc7c618, 0xc48511886a29e5d4, 0x5a9fc332e945ec0d, + ], + [ + 0xd5c5039bfdab07ac, 0x73730cf21db0bec7, 0xe2fd34d3877be799, 0xdc538336c73d259a, + 0xb011125563cf7c22, 0x1618ac92274d34c0, 0x1613ff2e9d614298, 0x0abc0d576592def4, + ], + [ + 0x99e5700d0d8d975e, 0x013ce65eea9ba2d9, 0xdcbc0bda9737d945, 0xd7b3e09377c62cbc, + 0x444bb01770512e6e, 0x7a4d19229e78ce29, 0x8f52c4f96ab0b3e6, 0x7e7af8bc0f9a5264, + ], + [ + 0xd2b1c4c22b8b06f6, 0x12296a03c0a1f5fa, 0x6b9f1ab655efa9f5, 0xfdb54306be32549b, + 0x1bd4be8920d9d6f7, 0x7988b4ce109526e8, 0xc7bdba32ac27a6e6, 0x63a1be7b9caea6a3, + ], + [ + 0xe4c88208671dbfa6, 0xd921d95f88286235, 0x2cdee0bc3298f5fa, 0x5a99ecca94c0758b, + 0xabb4abb5cb1333a5, 0xfa764ad90a664b5d, 0x4d6ce94227cdc197, 0x182b8f210e53156f, + ], + [ + 0xa59cf2cb1e159780, 0xad13ad70f3bbd12b, 0x57d5da5ec5a496d8, 0x4547766665e98107, + 0xd234cf73068735c9, 0x425b79a24215815a, 0xaec66414097da208, 0x24e9d6f1edc46d5e, + ], + [ + 0xf04deae2e232be5a, 0x1a3e7cda2aa4195f, 0x6e3d51644675c083, 0xbbb6a73ca97d51b8, + 0x58749685747c204a, 0x0b72d4b9dde231f7, 0xfd960d71bea42c50, 0x19fb4817f2025d18, + ], + [ + 0xa40a88918a1730a4, 0x5ffc59aca4187d05, 0xdc9a3965258b31a4, 0x2f1359ba684b1615, + 0x5e8cc956c5d3fd8d, 0x01b4f7e6f49fc6bc, 0x7bd89f2600037c2d, 0x936e00b9006e1d24, + ], + [ + 0x1e2f4226c5c5e9fe, 0xae52187f568eb96c, 0xb3c43aee36c355b2, 0xc90fa74de7b0565e, + 0xdedfd37dbd3468bb, 0xad78f2cbf0cd1ee7, 0x3c2c81f08027ff80, 0xa0c7610c7a72ccc9, + ], + [ + 0x70c2e6587f4f2b37, 0xb8803c35a7d54337, 0xa6e67b00124b6d1a, 0x81aaca9bd0bd1537, + 0x537a608be39ccecf, 0x22438da7034bda0c, 0x6822f0322b00af57, 0x3e9943be9999d9dc, + ], + [ + 0x243f8a5b3e0759c2, 0x56b6b9377982e13c, 0xaf52a311c9780125, 0x563b6505100a0dec, + 0xba0a1f76989dab7a, 0x9fe005ab7198e1cb, 0x550391105c126540, 0x51c29f959497a323, + ], + [ + 0xdb46d4e5a3a66b68, 0x34247b7c84dc3e23, 0xca576c07867fed14, 0x5669791252fe0695, + 0x84c33a215f44d184, 0xac17ab75c04ad95e, 0xc36f6b80b69481ff, 0x3e1ebcaaa58ebe07, + ], + [ + 0x282631602e935ab1, 0x400d33c3f2ddbf04, 0xa8dbe47cbf2c0a9c, 0x0d5cdac7bcc5fd0a, + 0x4070b733caedf941, 0x5ea4d9076941ba82, 0xcc97d8ab04173a8c, 0xa46734203bbda59d, + ], + [ + 0x0f4d0fc9b5a2078c, 0x71eb46f1e6671de3, 0xdd10e0e7952341d7, 0xb65fc68c68fe69be, + 0x1ad84fd23d2bb9e3, 0x5791f6cf5ff509a3, 0xb6f3b56d6b421565, 0x9b13caffc59d56bf, + ], + [ + 0x262bdfeebf961ec2, 0x326b66050c2f3bb5, 0xeba56b68c487478a, 0x5a1d702023a98544, + 0xd7755ce80c9c50c8, 0x3a14523af1ad4e81, 0xfc81a2bdf4d09934, 0x4d2809f0496057fb, + ], + [ + 0x051173f00e22c110, 0x1266611856a4aeb2, 0xb05138840872fb79, 0xc3299931b9a886da, + 0x3a6fc7b342c3fb8a, 0x22b075be09e1f8d8, 0xab13fc99f4ff7d5a, 0x3dc6224281841be6, + ], + [ + 0x937d121bc4771127, 0x84e718a117b56ffe, 0x633579ce1ef7a831, 0x78364b8592af35a4, + 0x2d27507cdd486422, 0x94bdf5c28fc78966, 0xf94ba30d953f3c59, 0xa4ca848e6be00a71, + ], + [ + 0x4272728069454710, 0x398048db59991170, 0xbff41537fc3d6e75, 0x20288d35a65bbb17, + 0x2daad38f2c3b1aab, 0x417bd902f81a1c55, 0xa1143154f8ac69af, 0x7ac4b958dee68a41, + ], + [ + 0x0df5c962d5ec5e6c, 0x94d5074f5f691b0c, 0xd5021e6b92598ba4, 0xe9df5f80af387fc8, + 0x0e13773313b48101, 0xabb2c26f21d448a4, 0x93cfb5484233c206, 0x01a6361a4bf8157c, + ], + [ + 0xd605f96633ddf187, 0xaa4248bd796be51d, 0xd450b453e35214d1, 0x0a20b2dacf6c8432, + 0xce7029e2e51ad670, 0xfed18221bbfe61d5, 0x0630421a8b9cf02c, 0x90d0f1928149d643, + ], + [ + 0xf568648ac5df5e8a, 0x4617ea08aa569a3e, 0xe16b8d06d516f663, 0xaf2bffa89a68be38, + 0x53020b5eb31fa5e9, 0x24f3181770de2ac8, 0x76c0b60ce41ee449, 0xa877c40c18ac0f34, + ], + [ + 0xb3ab62de4fe6c0ab, 0x4593160f1cb348a4, 0x985650161fbd6377, 0x7b9a1c27e9bc065d, + 0xfde15539c521fef8, 0x84b2b50e96d0a8f6, 0x5bc90c11f34be73d, 0x18faee7187c43a43, + ], + [ + 0x3f5142125cbea51e, 0x9efbdeb62fd9bbfc, 0x26bcee2c75c2e9ce, 0xe4622f8e7d6ba352, + 0xd3725a15c452d99e, 0xf8838921f45770b3, 0x727fe62814b18792, 0x50effc48e4c28dd7, + ], + [ + 0x631ea2bfcdc9b0e8, 0xbbe8ea9b22864fdd, 0x0a2d82b943df4650, 0x8eae41954ef13207, + 0xebdc90d3fc269352, 0x93895af9ca450504, 0x083a1d554e6382c5, 0x3457ede94952697c, + ], +]; + +pub(crate) const COMB_TABLE_Y: [[u64; 8]; 64] = [ + [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + [ + 0x78cd1e0f3ad80892, 0xd1ca2b2fa8f05406, 0x5bca4bd88a2763ae, 0xb2dcde494a5f485e, + 0xa000c55b881f8111, 0xf209f70024a57b1a, 0xc0eabfa9cf7822fd, 0x7dde385d566332ec, + ], + [ + 0x5bdd7095d4885960, 0x16b33f068a346e27, 0x964320386101fe2d, 0x30341b850799dcf9, + 0x0f1a7c953f7bf01e, 0x6750ed0eacc54360, 0x2ab0c607acaeb4c5, 0x141d802070d55dc9, + ], + [ + 0xb0d58ba3eb7a8d6b, 0x74ab9eacf841a31f, 0x2b09fe1c0d65e312, 0xec7ef9c53f13ca3f, + 0x91d389a628635d2c, 0xe377d4827a3f95c9, 0x4266f4003292bb34, 0x222a9f17d13158ba, + ], + [ + 0x57ccf52502877a21, 0xe18fed4b2203db8b, 0xa9bb5d5815065deb, 0x44c28d42f31ea9ea, + 0x228c078d2f2318c9, 0x970dc27735d8c29e, 0x2ee07e0c1c143bbd, 0xa0498b49bfcd4dd4, + ], + [ + 0x69e5585e792d484a, 0x06d46ee9ec2685b4, 0x0bd2a23887266898, 0x6fa5f875ce6017f7, + 0x15f187aea9db5120, 0xb0f596193b9249dd, 0xfafdce8c5252d5c4, 0x4108aef6e60a1824, + ], + [ + 0x46ed8dfb92f37489, 0x2d40e7d42b218fdc, 0x7a603a86cf8beed4, 0xeda4b2edf378685a, + 0x41c6adb984b3ccbf, 0x003fccd1dac369b4, 0xecf52a033db7a344, 0xaa3041953cd6d0d3, + ], + [ + 0xf646c283d8bbacaa, 0x46cfefb415cedccf, 0x7ab20b31945edb46, 0x1e417111e3e3b93c, + 0xbe65bc7db701e336, 0xe348e8a7876507fc, 0x6cb48e30d48f4866, 0x3656bf1ebe73e086, + ], + [ + 0x9629051d87f42d2c, 0xd23cb4f32a695290, 0x78453e4a0fe0ea27, 0x0f3ab1d7e83767a9, + 0xcd29328e4bb53243, 0x032c337096b6ff90, 0xc39a8d773a4feb52, 0x1e387c4ea5a2709e, + ], + [ + 0x6646f12039c4efa5, 0xe9e2e410ffae62e2, 0x82e589222ef55cff, 0x65dbae62a8290c89, + 0x7b6f2a87ca0f9ed7, 0x27add4659f9bdcce, 0xf4e967cf0f08c3ed, 0x26cb9c41ffd75c92, + ], + [ + 0xfb0040abe4ba5397, 0x241f6bb4859acfeb, 0x5ddc0ce1ce7b7f82, 0xfc163011c51a42af, + 0xea9eddfb532597f7, 0xde2356e2557085b3, 0xa2108ecaa07faeeb, 0x999a2b847db17651, + ], + [ + 0x1f3a00fd9da8b9b7, 0x741fca5abb6c4163, 0xc948a76b6ce2d72a, 0x6e4f7ec1a7ad191e, + 0x3a2c74063c19f7d1, 0x4a80b0090c4f23c5, 0xdda37e440834744e, 0x0b3de16e128dccfb, + ], + [ + 0xec7cbdf7f796a244, 0xbf9cfd8a24ff2d4b, 0x2f8aed9ce489e1be, 0x764e6b983bf434d9, + 0x5c3fa90cdb9809c1, 0x94127d7ff40b08c0, 0xefa9a69df68210dc, 0x6e1b0a14b87f46a6, + ], + [ + 0xb32a721f6932b99c, 0xa8aa353903e2731c, 0xf530aeef485d5eeb, 0xfc9decaf19607f77, + 0x5f15561be419d9ca, 0x2c59431f5933d7bc, 0xd4637fa4caeecec6, 0x20438919f6c040bd, + ], + [ + 0x5ae1f8c545fc5e16, 0x2285d04dc72e0d00, 0x6a42776ca9ad2f41, 0xe25af08e00887a59, + 0x948ea859eea359c2, 0xd0ebad6680022e1e, 0x317faebe3b8e9031, 0x9befacd063fd4706, + ], + [ + 0xe5eb47a64b96d6ea, 0x666f99de155ff15f, 0x0bc0e6b804e5bd21, 0xdb05e40004f0d349, + 0x73eaff1aa306f396, 0x34b6093399aa3208, 0x9431421061fcef6e, 0x9c9028cf33f4f134, + ], + [ + 0xa70565dea819445d, 0xf8bf9fc253e72ebc, 0x1041bb9a1eb90332, 0x467dbf47cb9f7992, + 0xde17ac6962c5a880, 0xdbadcce0973b6c7b, 0xdf064409d5fb3509, 0x3d8dc63f138c920f, + ], + [ + 0x9095a17575bf238b, 0xbe03a913345d664b, 0x62e144d25f9d8029, 0x1f25b4bfa8bd5d96, + 0xe7db57f1531f996a, 0x9b4c0b51c5e57c21, 0x1f32b0169c1ab06b, 0x337bb448ddcfe3f4, + ], + [ + 0x7215331e80e444d4, 0x8eda53d34b90de99, 0x7f6c3d748c970f2d, 0x8d21677488a6753c, + 0x6593666a9f25e0f6, 0xde319609defc5f94, 0xd2823d794e140475, 0x6ee0da1a0f561a9c, + ], + [ + 0x4c0cdb869ba92dd0, 0x8a93d34a562d0be4, 0xb20e861265e215b5, 0xd7c3e4cbc14d41b4, + 0x2683aca3ca10536a, 0xdb69ad109622013e, 0xf39dd6ead84e4642, 0x14d08062627f2f43, + ], + [ + 0x410a149bd7f63cc8, 0x0a6bc34cd81e94fa, 0xc9b6b958920263da, 0x31452493d4628f4c, + 0x8c9d2bd33511ce46, 0x4704e4ee6a17e690, 0xd0bd31ff561e78e5, 0x1b002fbbbe814e77, + ], + [ + 0xdd30f1d680b6d85f, 0x61d8a7ee85a614d6, 0xec2de97c1e2a49e4, 0x554d77039156873a, + 0xc5b0faf7aad4520e, 0xad76e7b139115d04, 0xc2cd745d37bc13d6, 0x3746f830231448c9, + ], + [ + 0xa3b931dbc4b7d1d7, 0xe93e7eb17ee32fa3, 0x3aa19b102d19dad4, 0x84a3686f2195f2a7, + 0xa194b516548d19d1, 0xe335d364e9512c2a, 0xd17cff876c9c9b55, 0x1537ef13af000833, + ], + [ + 0x3ccc2eb82eaf3326, 0x4a090c4e5973b1a4, 0x6ebbff128189248a, 0x9bb6edee661a3771, + 0x95bffb5d2004bd16, 0x92de45ffa334f8a0, 0xeb582532f173189b, 0x120f409ee314c163, + ], + [ + 0xe7e8a9a3a3afba55, 0x38f99690c9021617, 0x4d32770a79147b6e, 0x0c26d11aa5c7ee81, + 0xa961d9629fec17c9, 0x4263caa2d106ee78, 0xf15725f9d73f0305, 0x632a9c89d4e6d547, + ], + [ + 0xb0dc98181a0abd76, 0xc65a5bd5b7026363, 0x5d6139154bd7b151, 0x51ce1b709ee1325c, + 0xa26d9b528318d864, 0xfceaa434195655a4, 0x8b3615e38055a930, 0x16ad0d171dc806bb, + ], + [ + 0x3808f057005f9aed, 0xcdafc60342cf8f2f, 0xa856d8e8a5190006, 0xbda540aa952e0189, + 0x8fce584cc5e9fce6, 0x18781afc74acd7ce, 0x7430aac0a10ed2cd, 0x2751ec6336e8eb52, + ], + [ + 0xfffd933053927e05, 0x52b40a91b16a16a0, 0x4601f1e33f409d6d, 0xed4c8e11edd80e13, + 0xe7298ddf881b4a86, 0x628177ab11212397, 0xe76340bafa11af0b, 0x44f567df808d6f2b, + ], + [ + 0x0926cda3e8344d0a, 0x36217d5aca3a42fe, 0x05b3ec352bbeaf66, 0x1c2b277ea893c18b, + 0xad7741b0d8daf42e, 0x03ae990a27a9c49a, 0xaf5e1c26a43e83dc, 0x0c90d408379ee139, + ], + [ + 0x1b9eb8b37c933529, 0x0f8175bf7e5c45c4, 0xb97d2fecdf24e8dc, 0x4fe0a09b6ad58bf2, + 0x4bcaddde8227e332, 0x2e010605465657a7, 0x4f2c6be247a73574, 0x2b75111fec4c9d38, + ], + [ + 0x0d3e73f0176c1193, 0x22e03a191834c29f, 0x0cf298260e03b94d, 0xe9cb7cc17136069d, + 0x4a77873a67d7cf6d, 0x8fc9aefa9cfc7446, 0xd60a48c1ad22e546, 0x57a574c971725887, + ], + [ + 0x32579eb87e6c18a2, 0x32fb78fed0c1b935, 0x7f7aea3446ae087c, 0x9ed43718a9561ce8, + 0xc13c38b60ae96328, 0x522331856ea44f3e, 0x821c4bb1c3c4ad0d, 0x2c6f6bb3384a5630, + ], + [ + 0x8f59c5f759daa0c0, 0xa069da37b7bc6b07, 0x03956a019069a441, 0xe23ac2b6becc4918, + 0x7667f366335c55d1, 0x852a60f39609d940, 0xaaf43775b3dc0022, 0x5793aecfc949c2dc, + ], + [ + 0xa9029aee73e524b9, 0xe9e362f360656ad9, 0x9f65463be68407fb, 0x9d8204e1b00c8fe1, + 0x74a248a03dbf13eb, 0xe8290bab74762608, 0xce387c34035f6e30, 0x7cbca43c3bf9724d, + ], + [ + 0x22b80cb2f1a975e3, 0x1eaa6bc721eca6bb, 0x398456538b6e7ba7, 0x475e8d236dd7c396, + 0x4fec1b14da2469c8, 0x16cc9d83a5178efc, 0x3eb0a98efcceb3d4, 0x443c766122fd4ca3, + ], + [ + 0xe8cf4cfa500716f9, 0xdf7eecdee92150f8, 0x0e650b3083e82f7c, 0x2a6cbac9ac990ba5, + 0xad089e7b0d5a59a7, 0xed67ce90dcb29134, 0x172f1fd70c6093e3, 0x547a78f8409d7f19, + ], + [ + 0x3b9018d9c178ee7c, 0x2bbf7a88bfc52b99, 0x8a429813c394ef41, 0x2af8b3db88cabd59, + 0xe9db92ea5bde1431, 0x3c857c7f454f6db6, 0xad4773c8e9a1fe57, 0x9fe86533c33ef9ba, + ], + [ + 0x1dc8a910b46a3cf9, 0x25acb81669d6c5ee, 0x5f5d5448ebdc44ac, 0x8aa46972d79b486f, + 0x24ec0b669a360db4, 0x3ac3e560b6d4c6be, 0x3e7cfd9de04229ba, 0x5d383bbc55ba1043, + ], + [ + 0x28471dc257a1c112, 0x95ded7822e7ccb56, 0xcaef5c323e63be05, 0x87517baff92e77ea, + 0x1771b5aab5543f91, 0x1b663f69690f1e53, 0xefff97330d5ed882, 0x0d59aae84321eed6, + ], + [ + 0xa289c1f1f5888e66, 0xb4822dfbe6533016, 0xc2ab7931baffe4a7, 0x904c7809b7f70945, + 0x477817dc44c2ae10, 0x239e74194317d4c7, 0x762799a2733b6415, 0x31b114d35f367405, + ], + [ + 0x05c3d4ee3b3ff7bc, 0x0d8c829fa92ede1c, 0x8ab43ac8c7e5f214, 0x8431fb706d94f3c1, + 0xd3540966e380a228, 0x680a77e8926e4add, 0x7e57fc4418ed1c0a, 0x389977d95c1059ff, + ], + [ + 0x0a74212ac66e7bef, 0xe5ef36294d53a737, 0x5350ad99c741d6e1, 0xf1864cfb9f41ac99, + 0x1eebaa2825e6bb8b, 0xcc8a08aac3a20492, 0x34ea768de25b2b5a, 0x8ee3624d21d233b3, + ], + [ + 0x280f797ae6b6396b, 0x9795a028747f2204, 0x6e6dfc0a652d92b3, 0x245d08d6238396c6, + 0xb8229ec368e71de1, 0xc6133820dee92135, 0xda00e99f72047a7a, 0x002b8ae716e7ba52, + ], + [ + 0x7dbb96df3d4dcd8b, 0x81310ecc0611a777, 0xeff15f7badf4e420, 0xdf47038af4be54e4, + 0xa6688ab07f0053b4, 0xc30e5b6f73b116a0, 0xfbfb756443e3322a, 0x54356e4e8a5518a0, + ], + [ + 0x826e0126fafcd410, 0x553ffda0d223c5c4, 0x68791e0c0e53db56, 0x5d43483c52c76c68, + 0x99aeb54489fdcffe, 0xf6a73add0d3c039e, 0xb7a1a0fa5af206a4, 0x1dbcefca5a4daf31, + ], + [ + 0x88a3dda325bbd5b4, 0x93738f396768f246, 0xdfe032ad892876f0, 0x75c9aa5857cc90f8, + 0x08a05eced243d75e, 0x8626969ea4b8b033, 0xca605e65f31d6194, 0x206007cfed651ec7, + ], + [ + 0xed1e9e6887d34f37, 0xd21f17b004f45dc1, 0x7c350090a0c3cf67, 0x0fb6ce1629e00570, + 0x8d523d9ab193d40a, 0x44e65f9eafc7de66, 0xd304041ffcc4444b, 0x38546f86cbce4763, + ], + [ + 0xfe0bd52b03da6375, 0xf2aef047c8e2fc46, 0x2a06480c50f74c51, 0x41617e0e98322bdf, + 0x24df8675fe27025d, 0x24fe1b3222b1302b, 0xe4fdd76f78f727c2, 0x442450c0eacc78a0, + ], + [ + 0x94c947e038556ab7, 0x3ead23886d69b940, 0xa2adfb17d78c777d, 0x80bba4d91e93297c, + 0x60a8c22ede5efd9a, 0xa315184d7205b080, 0x90e47f5eef2e8dcf, 0x26660790f92e541d, + ], + [ + 0x36ddfdb5f441711f, 0x061f2ed14c20e29d, 0x87ac498a76484396, 0x7557d4a3a8551a0c, + 0x60bb13c93984a67c, 0xc9dd83de70a9fad9, 0x7b6891443ed9c9ec, 0x823654f76d859fb6, + ], + [ + 0x14217281437eedf0, 0xf5c7bea0561bff8e, 0xfb55caf1b7f75bcd, 0x5ed5c31bba595b2b, + 0xb51e90ec4eb391d1, 0xfe9adf741468992a, 0xe8ce10329f18d3f8, 0x30c55a4e8ae20044, + ], + [ + 0x52b19b59a685c789, 0xf794dffbd140ce1c, 0x2d2da845bf5eb818, 0x79b957a91b069c98, + 0x3ca174d3a2cee953, 0x143aaedb78345faa, 0x0f436a683e84327d, 0x11bbc5a43639bc8c, + ], + [ + 0x47f3eb9d657eea4e, 0xb29ac2efd38350c3, 0x8bdcc89ec3cd5135, 0xc71ebf2129aea98b, + 0x000a749dd2b4d84c, 0x7cc08ac6a671cdaf, 0x3c6bd426bdd97129, 0x69e68a9edb2871c1, + ], + [ + 0xca3c486323e6384d, 0x823be93bd13c68d2, 0x134546d1495408b5, 0x955ec4796e527007, + 0xd3da81325ae8df36, 0x0a9f35b55b942d3c, 0x2ab23636c38d122a, 0x354c185b38222f39, + ], + [ + 0xc7f7403581ed441d, 0xca1386983080b5a6, 0xa1a8fc8b8ae469fa, 0xc9ee4c184245bd22, + 0x59bc1c218a290b22, 0xbee95f2aadcf95f8, 0xb9b243b9008e74ca, 0x8f21deacd50894e6, + ], + [ + 0x7f382fa0a437e021, 0x7a3c392f40b73dd0, 0x7a4940ae518a3b7a, 0xc777d0a9dd9f2036, + 0x7dc753a0da641d78, 0x4451d19455667b37, 0xc04035385bb5a90e, 0x61437973f00fc9c8, + ], + [ + 0x06e888518b4b307f, 0x0feede58d8c8ee9b, 0x2e13b19ca54d5492, 0x83d9cf812bfd3188, + 0xea553fbb7fa9560b, 0xc9e1435309e52222, 0x87468979c61edfc8, 0x46950a399b065bbb, + ], + [ + 0x1fe97bf9fe528357, 0x3ac149154655a207, 0xf49cb9bd915cbc1d, 0x53ee745f4eb1fdbb, + 0x961b73178bd88bb1, 0xcd25ad0667d79222, 0x8b5f27e224800f01, 0x0bd4ca67d0ce1c61, + ], + [ + 0xf100a0a7098a9f30, 0x96269497ed901840, 0x3135a34d4f4900f3, 0x156ff09fe4dbf60f, + 0xf58a9ce1521e178f, 0xcb62c5b979288c20, 0x4fa7559a4bf14815, 0x67e22559ab79f5ca, + ], + [ + 0xd6e922a66a9391c9, 0xec7d951169c3dc09, 0xa241645e879da31c, 0x8d3c6ebd155a87be, + 0x0b8fb7cfcc508dd0, 0x1368f13031cd6538, 0x9557309267665c10, 0x0dd18b203d019b23, + ], + [ + 0x52d1e73f157e7e0d, 0x5f7048cb60c88b7d, 0x3d659814676d3be0, 0xc76df29925013183, + 0x3a4016b46fc1a190, 0xada0e1945ad9574d, 0xa5327e92d773ca9e, 0x14f7547d62a602a0, + ], + [ + 0x4bea965e81ee77a7, 0x43b27c4ff4a9dfee, 0x8a25476335dfd49f, 0x1195c302a4ff3da5, + 0x0ebeb72bd17810d5, 0xbcfcd8e0f957e90a, 0xe278e7b4d601c4f3, 0x6f6de8a413b96c02, + ], + [ + 0xc23d355b2a10f887, 0x23d89fd92aec37d3, 0x69b857fcf974f97c, 0x3ee15700d6c3ca48, + 0xf6b13c3e7c5b0f06, 0x55a8e48f06441e3e, 0x0e95af68770652f3, 0x910a74c5f94c7a17, + ], + [ + 0x25ea4a4cace96470, 0x2ca9f9e7990be9e9, 0xb1b6078df4c10c24, 0xd9ae9ee435fa68ad, + 0x63e2028e131a2df0, 0xe13a7b4686283827, 0xc402f90a59644c3f, 0x42efd5c3a2234f41, + ], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::bp512r1::Bp512r1FieldElement; + use crate::bp512r1_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::bp512r1_point::Bp512r1JacobianPoint; + + const WIDTH: usize = 6; + const BITS: usize = 512; + const D: usize = BITS.div_ceil(WIDTH); + const TABLE_SIZE: usize = 1 << WIDTH; + + /// Regenerates the comb table from scratch via the crate's own (independently verified) + /// point arithmetic, per this module's construction: `pow2[i] = 2^(i*D) * G`, then + /// `table[idx] = sum of pow2[b] for each bit b set in idx`. + #[test] + fn regenerated_table_matches_checked_in_constants() { + let g = Bp512r1JacobianPoint::from_affine( + Bp512r1FieldElement::from_limbs(G_X_LIMBS), + Bp512r1FieldElement::from_limbs(G_Y_LIMBS), + ); + + let mut pow2 = [g; WIDTH]; + for i in 1..WIDTH { + let mut p = pow2[i - 1]; + for _ in 0..D { + p = p.double(); + } + pow2[i] = p; + } + + let mut table = [Bp512r1JacobianPoint::INFINITY; TABLE_SIZE]; + for bit in (0..WIDTH).rev() { + let step = 1usize << bit; + let pw = pow2[bit]; + let mut i = step; + while i < TABLE_SIZE { + table[i] = table[i - step].add(&pw); + i += 2 * step; + } + } + + for (idx, point) in table.iter().enumerate() { + if idx == 0 { + assert!(point.is_infinity().to_bool(), "table[0] must be infinity"); + assert_eq!(COMB_TABLE_X[0], [0, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!(COMB_TABLE_Y[0], [0, 0, 0, 0, 0, 0, 0, 0]); + } else { + let (x, y) = point + .to_affine() + .unwrap_or_else(|| panic!("regenerated table[{idx}] is unexpectedly infinity")); + assert_eq!(x.to_limbs(), COMB_TABLE_X[idx], "table[{idx}].x mismatch"); + assert_eq!(y.to_limbs(), COMB_TABLE_Y[idx], "table[{idx}].y mismatch"); + } + } + } +} diff --git a/crypto/ec/src/bp512r1_domain.rs b/crypto/ec/src/bp512r1_domain.rs new file mode 100644 index 000000000..18c682855 --- /dev/null +++ b/crypto/ec/src/bp512r1_domain.rs @@ -0,0 +1,36 @@ +//! brainpoolP512r1 domain parameters, RFC 5639 §3.4. `p` lives in [`crate::bp512r1`]; `n` lives in +//! [`crate::bp512r1_scalar`]. This module holds the rest: the curve coefficients `A` and `B` and +//! the base point `G`. +//! +//! Unlike every NIST curve or secp256k1 in this crate, brainpool's `A` is neither `-3` nor `0`, so +//! it has no dedicated doubling-formula shortcut: [`crate::bp512r1_point`] uses the general-`a` +//! "dbl-2007-bl" formula instead. +//! +//! Every value below was extracted directly from a freshly downloaded copy of RFC 5639 §3.4's text +//! (concatenating the line-wrapped hex under `Curve-ID: brainpoolP512r1`, not retyped) and verified +//! in Python before any of this was ported to Rust: `y^2 = x^3 + Ax + B (mod p)` holds for `(G_X, +//! G_Y)`. + +/// The curve coefficient `A` in `y^2 = x^3 + Ax + B`, little-endian `u64` limbs. +pub const A_LIMBS: [u64; 8] = [ + 0xe7c1ac4d77fc94ca, 0x7f1117a72bf2c7b9, 0x0a2ef1c98b9ac8b5, 0x2ded5d5aa8253aa1, + 0xa83441caea9863bc, 0x94cbdd8d3df91610, 0xe2327145ac234cc5, 0x7830a3318b603b89, +]; + +/// The curve coefficient `B` in `y^2 = x^3 + Ax + B`, little-endian `u64` limbs. +pub const B_LIMBS: [u64; 8] = [ + 0x2809bd638016f723, 0x984050b75ebae5dd, 0x77fc94cadc083e67, 0x2bf2c7b9e7c1ac4d, + 0x8b9ac8b57f1117a7, 0xa8253aa10a2ef1c9, 0xea9863bc2ded5d5a, 0x3df91610a83441ca, +]; + +/// The base point `G`'s `x` coordinate, little-endian `u64` limbs. +pub const G_X_LIMBS: [u64; 8] = [ + 0x8b352209bcb9f822, 0x7c6d5047406a5e68, 0x50d1687b93b97d5f, 0xff3b1f78e2d0d48d, + 0xb43b62eef4d0098e, 0x85ed9f70b5d916c1, 0x5a21322e9c4c6a93, 0x81aee4bdd82ed964, +]; + +/// The base point `G`'s `y` coordinate, little-endian `u64` limbs. +pub const G_Y_LIMBS: [u64; 8] = [ + 0x78cd1e0f3ad80892, 0xd1ca2b2fa8f05406, 0x5bca4bd88a2763ae, 0xb2dcde494a5f485e, + 0xa000c55b881f8111, 0xf209f70024a57b1a, 0xc0eabfa9cf7822fd, 0x7dde385d566332ec, +]; diff --git a/crypto/ec/src/bp512r1_extra_bits.rs b/crypto/ec/src/bp512r1_extra_bits.rs new file mode 100644 index 000000000..a0e2e6a30 --- /dev/null +++ b/crypto/ec/src/bp512r1_extra_bits.rs @@ -0,0 +1,63 @@ +//! FIPS 186-5 Appendix A.4.1, "Conversion of a Bit String to an Integer mod n via Modular +//! Reduction" -- the "extra random bits" method both `bouncycastle-ecdsa`'s `keys_bp512r1`'s key generation +//! (Appendix A.2.1) and `bouncycastle-ecdsa`'s `ecdsa_bp512r1`'s randomised per-message secret (Appendix A.3.1) +//! use to turn a DRBG output wider than `n` into a value in `[1, n-1]` with negligible bias. +//! Identical algorithm to [`crate::p256_extra_bits`] -- see that module's docs for the full reasoning +//! -- with brainpoolP512r1's own `n` substituted. +//! +//! # Why brainpoolP512r1 needs this at all +//! +//! Like both other brainpool curves in this crate (and unlike P-384/P-521/secp256k1, whose `n` +//! passes Appendix A.4.1's bound with no extra bits and which draw them only for Appendix +//! A.3.1's `t >= 64` -- see `crate::p384_extra_bits`'s docs), this curve's `n` is not close to a +//! power of two. FIPS 186-5 Appendix A.4.1 step 2's bias-bound check +//! (`2ρ(1-ρ)(n-1) > ε·N`, `ε = 2^-64`) was evaluated directly (in Python, against `n`'s actual +//! value) for `N = 2^l` at increasing `l`: `l = 512` (matching `n`'s own bit length) fails the +//! check, and the smallest passing `l` is `575`; `bouncycastle-ecdsa` uses `l = 576` (72 bytes, +//! byte-aligned) as `bouncycastle-ecdsa`'s `keys_bp512r1::EXTRA_BITS_DRBG_OUTPUT_LEN`. +//! +//! The reduction itself is [`crate::barrett`]'s constant-time Barrett reduction with +//! `m = n - 1` (see [`crate::p256_extra_bits`]'s docs for the constant-time argument); `bp512r1_extra_bits_tests.rs` +//! pins known answers computed in Python and cross-checks it against the bit-serial algorithm this +//! module used to implement, kept there as an independent reference. + +use crate::barrett; +use crate::bp512r1_scalar::{Bp512r1Scalar, N_LIMBS}; +use crate::nat; + +/// `n - 1`, little-endian `u64` limbs. `N_LIMBS[0]` is odd (`n` is prime), so the subtraction +/// never borrows out of the low limb. +const N_MINUS_1_LIMBS: [u64; 8] = [ + N_LIMBS[0] - 1, + N_LIMBS[1], + N_LIMBS[2], + N_LIMBS[3], + N_LIMBS[4], + N_LIMBS[5], + N_LIMBS[6], + N_LIMBS[7], +]; + +/// `floor(2^1024 / (n-1)) - 2^512`: the low limbs of Barrett's `mu` for `m = n - 1` (whose top +/// limb is `mu`'s only other bit -- see [`crate::barrett`]), computed in Python from +/// RFC 5639 §3.4's `n`. +const MU_LOW_LIMBS: [u64; 8] = [ + 0x2fafac64db57db3a, 0x0eaf0d9015d5c4ce, 0x9ff38f5f59ee4710, 0xdb9470c61a235d44, + 0x666ad8f2f5bf92f7, 0x8373af60cc44ef09, 0x15d5ea2f03461e1e, 0x7f8d7f4ed6daeb8a, +]; + +/// FIPS 186-5 Appendix A.4.1 steps 3-5 for `n` = the curve order: reduces the big-endian bit string +/// `bytes` modulo `n-1` and adds `1`, landing in `[1, n-1]`. `bytes` may be up to `127` bytes +/// -- `2^1016 < (n-1) * 2^512`, Barrett's precondition -- which covers the 72-byte DRBG +/// output `bouncycastle-ecdsa` draws with room to spare; a longer input is a programming error and panics. +pub fn reduce_wide_bits_mod_n_minus_1(bytes: &[u8]) -> Bp512r1Scalar { + assert!( + bytes.len() < 128, + "reduce_wide_bits_mod_n_minus_1 given {} bytes, more than 127", + bytes.len() + ); + let t = barrett::limbs_from_be_bytes::<16>(bytes); + let reduced = barrett::reduce::<8, 16, 9>(&t, &N_MINUS_1_LIMBS, &MU_LOW_LIMBS); + let (plus_one, _) = nat::add(&reduced, &[1, 0, 0, 0, 0, 0, 0, 0]); + Bp512r1Scalar::from_limbs(plus_one) +} diff --git a/crypto/ec/src/bp512r1_mul.rs b/crypto/ec/src/bp512r1_mul.rs new file mode 100644 index 000000000..6a0a02baf --- /dev/null +++ b/crypto/ec/src/bp512r1_mul.rs @@ -0,0 +1,100 @@ +//! Constant-time variable-base scalar multiplication: `[k]Q` for a secret `k` and a point `Q` +//! that is not the base point -- a peer's public key, for the ECC CDH primitive of SP 800-56A +//! Rev. 3 Section 5.7.1.2 (`P = h d_A Q_B`). +//! +//! The crate's other two multipliers do not cover this case: [`crate::bp512r1_comb`] is fixed-base +//! (`[k]G` from a compile-time table of multiples of `G`), and [`crate::bp512r1_wnaf`] takes +//! [`crate::bp512r1_scalar::Bp512r1PublicScalar`] and is variable-time by design. This one takes +//! [`Bp512r1Scalar`], the secret type, and is branch-free in `k` throughout. +//! +//! # Fixed window, width 4 +//! +//! `[k]Q` is computed left to right over `ROUNDS = ceil(BITS / 4)` windows of 4 bits: each round +//! doubles the accumulator four times and adds `[w]Q` for the round's window value `w`, read off +//! `k`'s limbs at positions fixed by the loop (so *which* bits are read is public, only their +//! values are secret). `[w]Q` comes from a table of `[0]Q .. [15]Q` built per call in Jacobian +//! coordinates -- entry 0 is the identity, the rest by doubling and adding `Q` -- and is read by +//! `table_lookup`, which scans every entry under a mask rather than indexing by `w`, as the +//! comb's lookup does. The accumulator's addition is the complete [`Bp512r1JacobianPoint::add`]: it +//! masks in the identity, same-point and opposite-point cases, so the accumulator may be the +//! identity (it starts there and stays there through leading zero windows), a table entry may be +//! the identity (`w = 0`), and `Q` itself may be added to a multiple of `Q`; the comb's "no +//! offset" argument carries over unchanged. +//! +//! Cost: `4 * ROUNDS` doublings, `ROUNDS` full additions, `ROUNDS` masked scans of the table and +//! 14 point operations to build it: on P-256 about three times the fixed-base comb (whose table +//! is precomputed and whose additions are mixed) and 1.3 times the variable-time wNAF multiplier +//! (136 us against 44 us and 106 us in `p256_point_benches` / `p256_scalar_benches`). The table +//! is `16 * size_of::()` bytes of stack. Width 4 over width 5 keeps +//! that table at 1.5 KiB here and 3.4 KiB on P-521 for one fewer addition in five; a Montgomery +//! ladder would need an addition per bit. The table and the working accumulator are scrubbed +//! before returning; the caller owns the result and scrubs it when it is done. +//! +//! Verified against [`crate::bp512r1_comb`] (for `Q = G`), against [`crate::bp512r1_wnaf`] and against +//! `[a k mod n]G` (for `Q = [a]G`) in `bp512r1_mul_tests.rs`, including `k = 0`, `k = 1`, `k = n-1`, +//! `Q` the identity, `Q` negated and `Q` in a non-canonical-`Z` representation. + +use crate::bp512r1_point::{Bp512r1JacobianPoint, select_point}; +use crate::bp512r1_scalar::Bp512r1Scalar; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 4; +const BITS: usize = 512; +const ROUNDS: usize = BITS.div_ceil(WIDTH); +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]Q`, branch-free in `k`. `Q` may be any point, including the identity; `k` may be any +/// value in `[0, n-1]`, and `[0]Q` is the identity. +pub fn multiply_point(k: &Bp512r1Scalar, q: &Bp512r1JacobianPoint) -> Bp512r1JacobianPoint { + let mut table = [Bp512r1JacobianPoint::INFINITY; TABLE_SIZE]; + table[1] = *q; + for i in 2..TABLE_SIZE { + // Which entry is built how depends only on the (public) index, never on `k` or `Q`. + table[i] = if i % 2 == 0 { table[i / 2].double() } else { table[i - 1].add(q) }; + } + + let limbs = k.limbs(); + let mut r = Bp512r1JacobianPoint::INFINITY; + for round in 0..ROUNDS { + let low_bit = (ROUNDS - 1 - round) * WIDTH; + let window = window_at(limbs, low_bit); + for _ in 0..WIDTH { + r = r.double(); + } + let mut entry = table_lookup(&table, window); + r = r.add(&entry); + entry.zeroize(); + } + + for entry in table.iter_mut() { + entry.zeroize(); + } + r +} + +/// The `WIDTH` bits of `limbs` from bit `low_bit` up, as a window value in `0..TABLE_SIZE`. Bits +/// at or above `BITS` read as zero, so on a curve whose `BITS` is not a multiple of `WIDTH` +/// (P-521) the top window sees only the scalar. The bit positions are public; the shifts and +/// masks are branch-free in the limb values. +fn window_at(limbs: &[u64; 8], low_bit: usize) -> usize { + let mut window = 0usize; + for j in (low_bit..low_bit + WIDTH).rev() { + let bit = if j < BITS { (limbs[j / 64] >> (j % 64)) & 1 } else { 0 }; + window = (window << 1) | (bit as usize); + } + window +} + +/// Table entry `secret_index`, selected by scanning every entry under a mask so the index never +/// steers which memory is read. +fn table_lookup( + table: &[Bp512r1JacobianPoint; TABLE_SIZE], + secret_index: usize, +) -> Bp512r1JacobianPoint { + let mut r = Bp512r1JacobianPoint::INFINITY; + for (i, entry) in table.iter().enumerate() { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + r = select_point(is_this_entry, entry, &r); + } + r +} diff --git a/crypto/ec/src/bp512r1_point.rs b/crypto/ec/src/bp512r1_point.rs new file mode 100644 index 000000000..7e039edec --- /dev/null +++ b/crypto/ec/src/bp512r1_point.rs @@ -0,0 +1,399 @@ +//! Branch-free Jacobian-coordinate point arithmetic on the brainpoolP512r1 curve `y^2 = x^3 + Ax + +//! B` (RFC 5639 §3.4). Unlike every NIST curve (`a = -3`) or secp256k1 (`a = 0`) in this crate, +//! brainpool's `A` is a "random" value with neither shortcut available. +//! +//! [`Bp512r1JacobianPoint::add`]'s exceptional-case handling (infinity, same-point, opposite-point) +//! is identical to [`crate::p256_point::P256JacobianPoint::add`] -- see that module's docs for the +//! full derivation and verification methodology, which applies unchanged here, since `generic_add` +//! doesn't depend on `a`. Only [`Bp512r1JacobianPoint::double`] differs: this is the general-`a` +//! doubling formula ("dbl-2007-bl" in the hyperelliptic.org explicit-formulas database): `XX = +//! X1^2`, `YY = Y1^2`, `YYYY = YY^2`, `ZZ = Z1^2`, `S = 2*((X1+YY)^2-XX-YYYY)`, `M = 3*XX + +//! a*ZZ^2`, `T = M^2 - 2*S`, `X3 = T`, `Y3 = M*(S-T) - 8*YYYY`, `Z3 = (Y1+Z1)^2 - YY - ZZ`, +//! verified (not checked in) against the affine doubling law over 500 random on-curve points, +//! using brainpoolP512r1's actual `A`, before being ported here. + +use crate::bp512r1::Bp512r1FieldElement; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// The curve coefficient `A` in the field's Montgomery representation (`A * 2^512 mod p`), +/// computed in Python from RFC 5639 §3.4's `A` and pinned against +/// [`Bp512r1FieldElement::from_limbs`] by this module's tests. [`Bp512r1JacobianPoint::double`] +/// needs `A` on every call; converting the plain `A_LIMBS` into Montgomery form each time cost a +/// full field multiplication per doubling. +const A_MONTGOMERY_LIMBS: [u64; 8] = [ + 0xda1f8a34ea10c446, 0x14e4957dafa7d283, 0x40b04b724675bbab, 0xcf8f01119e6e87ff, + 0xa5ec30c83f80d1c7, 0x182d0f59f41e8778, 0xb83b84fae2d0850c, 0x5ec4f187227d2a83, +]; + +/// A point on the brainpoolP512r1 curve in Jacobian coordinates. +#[derive(Clone, Copy, Debug)] +pub struct Bp512r1JacobianPoint { + /// `X`, the Jacobian `X` coordinate. + pub x: Bp512r1FieldElement, + /// `Y`, the Jacobian `Y` coordinate. + pub y: Bp512r1FieldElement, + /// `Z`, the Jacobian `Z` coordinate; `Z == 0` denotes the point at infinity. + pub z: Bp512r1FieldElement, +} + +impl Bp512r1JacobianPoint { + /// The point at infinity, the identity of the curve's group. + pub const INFINITY: Self = Self { + x: Bp512r1FieldElement::ONE, + y: Bp512r1FieldElement::ONE, + z: Bp512r1FieldElement::ZERO, + }; + + /// Builds a Jacobian point from affine coordinates (`Z = 1`). Does not check that `(x, y)` is + /// actually on the curve -- see [`crate::bp512r1_sec1`] for that. + pub fn from_affine(x: Bp512r1FieldElement, y: Bp512r1FieldElement) -> Self { + Self { x, y, z: Bp512r1FieldElement::ONE } + } + + /// TRUE iff this is the point at infinity. + pub fn is_infinity(&self) -> Condition { + self.z.is_zero() + } + + /// Converts to affine `(x, y)` coordinates, or `None` for the point at infinity. Not constant + /// time: see [`crate::p256_point::P256JacobianPoint::to_affine`]'s docs. + pub fn to_affine(&self) -> Option<(Bp512r1FieldElement, Bp512r1FieldElement)> { + if self.is_infinity().to_bool() { + return None; + } + let z_inv = self.z.invert(); + let z_inv2 = z_inv.mul(&z_inv); + let z_inv3 = z_inv2.mul(&z_inv); + Some((self.x.mul(&z_inv2), self.y.mul(&z_inv3))) + } + + /// `-self`: negating the `Y` coordinate. + pub fn negate(&self) -> Self { + Self { x: self.x, y: self.y.negate(), z: self.z } + } + + /// Overwrites all three coordinates with zero (which is not the identity's encoding), for + /// scrubbing a secret-derived point before it goes out of scope; see + /// [`Bp512r1FieldElement::zeroize`]. + pub fn zeroize(&mut self) { + self.x.zeroize(); + self.y.zeroize(); + self.z.zeroize(); + } + + /// `2 * self`, via the general-`a` doubling formula ("dbl-2007-bl") described in the module + /// docs. + pub fn double(&self) -> Self { + let a = Bp512r1FieldElement::from_internal_limbs(A_MONTGOMERY_LIMBS); + let xx = self.x.mul(&self.x); // XX = X1^2 + let yy = self.y.mul(&self.y); // YY = Y1^2 + let yyyy = yy.mul(&yy); // YYYY = YY^2 + let zz = self.z.mul(&self.z); // ZZ = Z1^2 + let x1_plus_yy = self.x.add(&yy); + let s = x1_plus_yy.mul(&x1_plus_yy).sub(&xx).sub(&yyyy); + let s = s.add(&s); // S = 2*((X1+YY)^2 - XX - YYYY) + let m = xx.add(&xx).add(&xx).add(&a.mul(&zz.mul(&zz))); // M = 3*XX + a*ZZ^2 + let t = m.mul(&m).sub(&s).sub(&s); // T = M^2 - 2*S + let x3 = t; + let yyyy8 = + yyyy.add(&yyyy).add(&yyyy).add(&yyyy).add(&yyyy).add(&yyyy).add(&yyyy).add(&yyyy); + let y3 = m.mul(&s.sub(&t)).sub(&yyyy8); // Y3 = M*(S-T) - 8*YYYY + let y1_plus_z1 = self.y.add(&self.z); + let z3 = y1_plus_z1.mul(&y1_plus_z1).sub(&yy).sub(&zz); // Z3 = (Y1+Z1)^2 - YY - ZZ + Self { x: x3, y: y3, z: z3 } + } + + /// The generic (`self != ±other`, neither infinite) Jacobian addition formula, plus the `H` + /// and `R` intermediates [`Self::add`] uses to detect the exceptional cases. See + /// [`crate::p256_point::P256JacobianPoint`]'s private `generic_add`'s docs -- this formula + /// doesn't depend on `a` at all, so it's identical to the NIST curves' version. + fn generic_add(&self, other: &Self) -> (Self, Bp512r1FieldElement, Bp512r1FieldElement) { + let z2_sq = other.z.mul(&other.z); + let u1 = self.x.mul(&z2_sq); + let s1 = self.y.mul(&other.z).mul(&z2_sq); + let z1_sq = self.z.mul(&self.z); + let u2 = other.x.mul(&z1_sq); + let s2 = other.y.mul(&self.z).mul(&z1_sq); + + let h = u1.sub(&u2); + let r = s1.sub(&s2); + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); + let v = h_squared.mul(&u1); + let g_neg = g.negate(); + let acc = s1.mul(&g_neg); + let g2 = g_neg.add(&v).add(&v); + let x3 = r.mul(&r).sub(&g2); + let y3_temp = v.sub(&x3); + let y3 = acc.add(&y3_temp.mul(&r)); + let z3 = h.mul(&self.z).mul(&other.z); + + (Self { x: x3, y: y3, z: z3 }, h, r) + } + + /// `self + other`, branch-free per [`crate::p256_point::P256JacobianPoint::add`]'s docs. + pub fn add(&self, other: &Self) -> Self { + let self_is_infinity = self.is_infinity(); + let other_is_infinity = other.is_infinity(); + + let (generic, h, r) = self.generic_add(other); + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + // Dropping the `!` here (making `is_opposite_point` collapse onto `is_same_point`) is an + // accepted mutant, not a bug: when `h == 0`, `generic_add`'s own `z3 = h*self.z*other.z` + // is already `0` regardless of `r`, so `generic` is already infinity-representing in the + // true opposite-point case (`h == 0, r != 0`) even without this override selecting + // `Self::INFINITY` explicitly -- see `crate::p521_point`'s identical case for the full + // argument. + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for an `other` given in **affine** coordinates (`Z2 = 1`), or the identity + /// when `other_is_infinity` is TRUE (`x2`/`y2` are then ignored). The same group law and the + /// same branch-free discipline as [`Self::add`] -- generic-add and doubling candidates computed + /// unconditionally, the exceptional cases masked in -- but with `Z2 = 1` the generic formula + /// simplifies: `U1 = X1`, `S1 = Y1` and `Z3 = H * Z1`, which drops five of its sixteen field + /// multiplications. The fixed-base comb multiplier adds a table entry to its accumulator on + /// every round, and every table entry is affine or the identity, so it is the caller this + /// exists for; `pub(crate)` because a caller has to know its point really has `Z = 1`. + pub(crate) fn add_affine( + &self, + x2: &Bp512r1FieldElement, + y2: &Bp512r1FieldElement, + other_is_infinity: Condition, + ) -> Self { + let self_is_infinity = self.is_infinity(); + + // The generic mixed addition, computed unconditionally: `U1 = X1`, `S1 = Y1`. + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2 + let r = self.y.sub(&s2); // S1 - S2 + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); // R^2 + H^3 - 2V + let y3 = acc.add(&v.sub(&x3).mul(&r)); // -S1*H^3 + (V - X3)*R + let z3 = h.mul(&self.z); // H * Z1 * Z2 with Z2 = 1 + let generic = Self { x: x3, y: y3, z: z3 }; + + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + let other = Self { x: *x2, y: *y2, z: Bp512r1FieldElement::ONE }; + + // Same priority order as `add`: the identity cases override everything else. + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, &other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for points that are **public**, branching on the exceptional cases instead + /// of computing every candidate and masking. + /// + /// [`Self::add`] must evaluate the doubling candidate on every call, because on secret inputs + /// it cannot branch on whether the doubling case applies -- that costs a full point doubling + /// (roughly a third of the addition) on every addition, whether or not it is ever used. This + /// version pays it only when the operands really are the same point, which for a scalar + /// multiplier over distinct precomputed multiples is essentially never. + /// + /// Restricted to `pub(crate)` and used only by [`crate::bp512r1_wnaf`], whose scalars are + /// [`crate::bp512r1_scalar::Bp512r1PublicScalar`] and whose points are a signer's public key + /// and the curve's own base point: every value it branches on is already known to an attacker. + /// Do not call this on anything derived from a private key or a per-message secret -- the + /// constant-time [`Self::add`] exists for that, and the scalar type split is what keeps the two + /// multipliers from being confused for one another. + pub(crate) fn add_vartime(&self, other: &Self) -> Self { + if self.is_infinity().to_bool() { + return *other; + } + if other.is_infinity().to_bool() { + return *self; + } + + let (generic, h, r) = self.generic_add(other); + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + generic + } + /// `self + (x2, y2)` for a **public**, affine `other` (`Z2 = 1`): [`Self::add_vartime`]'s + /// early-exit structure over [`Self::add_affine`]'s mixed formula, for the variable-time + /// multiplier's fixed side, whose precomputed multiples of `G` are stored affine. + /// `pub(crate)` for both of the reasons its two parents are. + pub(crate) fn add_vartime_affine( + &self, + x2: &Bp512r1FieldElement, + y2: &Bp512r1FieldElement, + ) -> Self { + if self.is_infinity().to_bool() { + return Self::from_affine(*x2, *y2); + } + + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2, with U1 = X1 + let r = self.y.sub(&s2); // S1 - S2, with S1 = Y1 + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); + let y3 = acc.add(&v.sub(&x3).mul(&r)); + let z3 = h.mul(&self.z); // H * Z1, with Z2 = 1 + Self { x: x3, y: y3, z: z3 } + } +} + +pub(crate) fn select_point( + cond: Condition, + a: &Bp512r1JacobianPoint, + b: &Bp512r1JacobianPoint, +) -> Bp512r1JacobianPoint { + Bp512r1JacobianPoint { + x: Bp512r1FieldElement::from_internal_limbs(select_limbs( + cond, + &a.x.internal_limbs(), + &b.x.internal_limbs(), + )), + y: Bp512r1FieldElement::from_internal_limbs(select_limbs( + cond, + &a.y.internal_limbs(), + &b.y.internal_limbs(), + )), + z: Bp512r1FieldElement::from_internal_limbs(select_limbs( + cond, + &a.z.internal_limbs(), + &b.z.internal_limbs(), + )), + } +} + +fn select_limbs(cond: Condition, a: &[u64; 8], b: &[u64; 8]) -> [u64; 8] { + let mut out = [0u64; 8]; + ct::conditional_select(cond, a, b, &mut out); + out +} + +// `add_vartime` is `pub(crate)` -- deliberately unreachable from outside the crate, since calling +// it on a secret point would undo the constant-time discipline [`Bp512r1JacobianPoint::add`] exists +// for -- so no integration test can reach it. Its whole contract is that it computes the same group +// law as `add`, differing only in *how* it gets there, so that is what is pinned here: agreement on +// every exceptional case, including the ones a scalar multiplier over distinct precomputed +// multiples essentially never reaches by chance (a point added to itself, and a point added to its +// own negation). QUALITY_AND_STYLE's private-function carve-out applies. +#[cfg(test)] +mod tests { + use super::*; + use crate::bp512r1_domain::A_LIMBS; + + #[test] + fn a_montgomery_constant_is_a_in_montgomery_form() { + assert_eq!( + Bp512r1FieldElement::from_internal_limbs(A_MONTGOMERY_LIMBS), + Bp512r1FieldElement::from_limbs(A_LIMBS) + ); + } + use crate::bp512r1_domain::{G_X_LIMBS, G_Y_LIMBS}; + + #[test] + fn add_affine_agrees_with_add_on_every_case() { + let g = Bp512r1JacobianPoint::from_affine( + Bp512r1FieldElement::from_limbs(G_X_LIMBS), + Bp512r1FieldElement::from_limbs(G_Y_LIMBS), + ); + // A non-canonical-Z representation of G as well: `add_affine` reads `self.z`, so `self` + // must not be assumed affine even though `other` is. + let z = Bp512r1FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = Bp512r1JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + let selves = [g, g.double(), g.negate(), Bp512r1JacobianPoint::INFINITY, scaled_g]; + let others = [g, g.double(), g.negate()]; + for a in selves { + for b in others { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_affine(&bx, &by, Condition::::FALSE).to_affine(), + a.add(&b).to_affine(), + "add_affine disagrees with add" + ); + } + // The identity flag must win regardless of the coordinates passed alongside it. + assert_eq!( + a.add_affine(&g.x, &g.y, Condition::::TRUE).to_affine(), + a.to_affine(), + "add_affine with the identity flag must return self" + ); + } + } + + #[test] + fn add_vartime_affine_agrees_with_add_on_every_case() { + let g = Bp512r1JacobianPoint::from_affine( + Bp512r1FieldElement::from_limbs(G_X_LIMBS), + Bp512r1FieldElement::from_limbs(G_Y_LIMBS), + ); + let z = Bp512r1FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = Bp512r1JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + for a in [g, g.double(), g.negate(), Bp512r1JacobianPoint::INFINITY, scaled_g] { + for b in [g, g.double(), g.negate()] { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_vartime_affine(&bx, &by).to_affine(), + a.add(&b).to_affine(), + "add_vartime_affine disagrees with add" + ); + } + } + } + + #[test] + fn add_vartime_agrees_with_add_on_every_case() { + let g = Bp512r1JacobianPoint::from_affine( + Bp512r1FieldElement::from_limbs(G_X_LIMBS), + Bp512r1FieldElement::from_limbs(G_Y_LIMBS), + ); + // G, 2G, -G and the identity cover all four of `add`'s exceptional cases pairwise, plus + // the ordinary one (e.g. G + 2G). + let points = [g, g.double(), g.negate(), Bp512r1JacobianPoint::INFINITY]; + for a in points { + for b in points { + assert_eq!( + a.add_vartime(&b).to_affine(), + a.add(&b).to_affine(), + "add_vartime disagrees with add" + ); + } + } + } +} diff --git a/crypto/ec/src/bp512r1_scalar.rs b/crypto/ec/src/bp512r1_scalar.rs new file mode 100644 index 000000000..c4e1e7699 --- /dev/null +++ b/crypto/ec/src/bp512r1_scalar.rs @@ -0,0 +1,280 @@ +//! The brainpoolP512r1 scalar field, arithmetic mod the curve order `n` (RFC 5639 §3.4). Like +//! [`crate::bp512r1`], `n` here has no special algebraic structure, so this is built on +//! [`crate::montgomery`]'s generic Montgomery multiplication rather than a per-curve fold -- +//! identical in shape to every other curve's own scalar field in this crate (e.g. +//! [`crate::p256_scalar`]), just delegating the widening-multiply/REDC work to the shared module +//! instead of hand-writing it again. + +use crate::montgomery; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +/// `n`, the order of the brainpoolP512r1 base point `G`, little-endian `u64` limbs (RFC 5639 +/// §3.4). +pub const N_LIMBS: [u64; 8] = [ + 0xb58796829ca90069, 0x1db1d381085ddadd, 0x418661197fac1047, 0x553e5c414ca92619, + 0xd6639cca70330870, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, +]; + +/// `-n^-1 mod 2^64`, the Montgomery reduction constant. +const N_PRIME: u64 = 0xad49541f0f1b7027; + +/// `R^2 mod n` (`R = 2^512`), used to bring a plain value into Montgomery form. +const R_SQUARED_LIMBS: [u64; 8] = [ + 0xd2a3681ecda81671, 0x0886b75895283ddd, 0x3ec64bd033b7627f, 0xa6f230c72f0207e8, + 0xd7f9cc263b790de3, 0x723c37a22f16bbdf, 0x95df1b4c194b2e56, 0xa794586a718407b0, +]; + +/// `R mod n`, also the Montgomery representation of `1`. +const R_MOD_N_LIMBS: [u64; 8] = [ + 0x4a78697d6356ff97, 0xe24e2c7ef7a22522, 0xbe799ee68053efb8, 0xaac1a3beb356d9e6, + 0x299c63358fccf78f, 0x34cf724c4c362df1, 0xc02b1951cc3603f8, 0x5522624724163b74, +]; + +/// `n - 2`, little-endian `u64` limbs -- the public exponent [`Bp512r1ScalarField::invert`] +/// raises its base to. +const N_MINUS_2_LIMBS: [u64; 8] = [ + 0xb58796829ca90067, 0x1db1d381085ddadd, 0x418661197fac1047, 0x553e5c414ca92619, + 0xd6639cca70330870, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, +]; + +/// An element of the brainpoolP512r1 scalar field, i.e. an integer mod `n`. Stored internally in +/// Montgomery form; see the module docs. +#[derive(Clone, Copy, Debug)] +pub struct Bp512r1ScalarField([u64; 8]); + +impl Bp512r1ScalarField { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0, 0, 0, 0, 0]); + + /// The multiplicative identity. Its Montgomery form is `R mod n`, not the literal integer `1`. + pub const ONE: Self = Self(R_MOD_N_LIMBS); + + /// Builds a scalar-field element from little-endian `u64` limbs (an ordinary, non-Montgomery + /// value), reducing once if `limbs >= n` (valid since `limbs < 2^512 < 2n`), then converting + /// to Montgomery form. + pub fn from_limbs(limbs: [u64; 8]) -> Self { + let (diff, borrow) = crate::nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 8]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + let t = montgomery::widening_mul::<8, 16>(&reduced, &R_SQUARED_LIMBS); + Self(Self::finish_redc(&t)) + } + + /// Returns the canonical little-endian `u64` limbs (an ordinary, non-Montgomery value), `< n`. + pub fn to_limbs(&self) -> [u64; 8] { + let mut t = [0u64; 16]; + t[..8].copy_from_slice(&self.0); + montgomery::redc::<8, 16, 17>(&t, &N_LIMBS, N_PRIME).0 + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + crate::nat::is_zero(&self.0) + } + + /// `self + other mod n`. Operates directly on the Montgomery-form limbs: no REDC needed. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = crate::nat::add(&self.0, &other.0); + let (sum_plus_r, _) = crate::nat::add(&sum, &R_MOD_N_LIMBS); + let (diff, borrow) = crate::nat::sub(&sum, &N_LIMBS); + let mut when_no_carry = [0u64; 8]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 8]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_r, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod n`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = crate::nat::sub(&self.0, &other.0); + let (corrected, _) = crate::nat::add(&diff, &N_LIMBS); + let mut result = [0u64; 8]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod n`. + pub fn negate(&self) -> Self { + let (diff, _) = crate::nat::sub(&N_LIMBS, &self.0); + let mut result = [0u64; 8]; + ct::conditional_select(self.is_zero(), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod n`, via Montgomery multiplication. + pub fn mul(&self, other: &Self) -> Self { + let t = montgomery::widening_mul::<8, 16>(&self.0, &other.0); + Self(Self::finish_redc(&t)) + } + + /// Overwrites this value with zero, through a write the compiler may not elide. + /// + /// For the signing path, where a value of this type holds `d`, `k` or `k^-1` and would + /// otherwise stay legible on the stack after the signature is returned. This type is `Copy` + /// and carries public values too (verification's `u`, `v`, `e`, `r`, `s`), so it is not + /// wrapped in [`bouncycastle_utils::secret::Secret`] -- that would cost its `Copy`, its `const + /// ZERO`/`ONE`, and a scrub on every intermediate including the public ones. The trade is that + /// scrubbing is the caller's job, on every path out of the function; see + /// `bouncycastle_ecdsa`'s `sign_with_k` for the intended use. + /// + /// This does not reach values the compiler kept in registers or spilled itself, and is + /// defence in depth rather than a guarantee. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`montgomery::widening_square`]. Used by the exponentiation loops, not by point + /// arithmetic; see [`crate::p256::P256FieldElement::square`] for the measurement behind that + /// split. + pub fn square(&self) -> Self { + let t = montgomery::widening_square::<8, 16>(&self.0); + Self(Self::finish_redc(&t)) + } + + /// Converts a secret scalar (`d` or `k`) into Montgomery form for field arithmetic. See + /// [`crate::p256_scalar::P256ScalarField::from_secret`]'s docs. + pub fn from_secret(secret: &Bp512r1Scalar) -> Self { + Self::from_limbs(*secret.limbs()) + } + + /// `self^-1 mod n`, or `0` if `self` is `0`. Fermat's little theorem (`n` is prime) over the + /// public exponent `n-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..8).rev() { + let limb = N_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } + + /// [`montgomery::redc`]'s `(high, extra)` result, reduced to the canonical `< n` value. See + /// [`crate::bp512r1::Bp512r1FieldElement`]'s identical `finish_redc` for the invariant this + /// relies on and how it was verified for this specific `n`. + fn finish_redc(t: &[u64; 16]) -> [u64; 8] { + let (high, extra) = montgomery::redc::<8, 16, 17>(t, &N_LIMBS, N_PRIME); + let (sum, _) = crate::nat::add(&high, &R_MOD_N_LIMBS); + let (diff, borrow) = crate::nat::sub(&high, &N_LIMBS); + let mut when_no_extra = [0u64; 8]; + ct::conditional_select( + Condition::::from_lsb(borrow), + &high, + &diff, + &mut when_no_extra, + ); + let mut result = [0u64; 8]; + ct::conditional_select( + Condition::::from_lsb(extra), + &sum, + &when_no_extra, + &mut result, + ); + result + } +} + +impl PartialEq for Bp512r1ScalarField { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. Comparing Montgomery-form limbs directly is valid: the map `x -> x*R mod n` is a + /// bijection. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..8 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for Bp512r1ScalarField {} + +fn reduce_once(limbs: [u64; 8]) -> [u64; 8] { + let (diff, borrow) = crate::nat::sub(&limbs, &N_LIMBS); + let mut result = [0u64; 8]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut result); + result +} + +/// A scalar mod `n`, `< n`, held in `Secret`. See [`crate::p256_scalar::P256Scalar`]'s docs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Bp512r1Scalar(Secret<[u64; 8]>); + +impl Bp512r1Scalar { + /// Builds a secret scalar from little-endian `u64` limbs, reducing once if `limbs >= n`. + pub fn from_limbs(limbs: [u64; 8]) -> Self { + let mut secret = Secret::<[u64; 8]>::new(); + *secret = reduce_once(limbs); + Self(secret) + } + + /// Builds a secret scalar from SEC 1 §2.3.7 big-endian octets, reducing once if `>= n`. + pub fn from_be_bytes(bytes: &[u8; 64]) -> Self { + Self::from_limbs(crate::bp512r1_sec1::limbs_from_be_bytes(bytes)) + } + + /// Encodes to SEC 1 §2.3.7 big-endian octets. + pub fn to_be_bytes(&self) -> [u8; 64] { + crate::bp512r1_sec1::be_bytes_from_limbs(&self.0) + } + + /// The scalar's limbs, for the crate's own multiplier and field-arithmetic implementations to + /// read. Not exposed outside the crate. + pub(crate) fn limbs(&self) -> &[u64; 8] { + &self.0 + } +} + +/// A scalar mod `n`, `< n`, held as a plain (non-secret) value. See +/// [`crate::p256_scalar::P256PublicScalar`]'s docs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Bp512r1PublicScalar([u64; 8]); + +impl Bp512r1PublicScalar { + /// Builds a public scalar from little-endian `u64` limbs, reducing once if `limbs >= n`. + pub fn from_limbs(limbs: [u64; 8]) -> Self { + Self(reduce_once(limbs)) + } + + /// Returns the scalar's little-endian `u64` limbs, `< n`. + pub fn to_limbs(&self) -> [u64; 8] { + self.0 + } + + /// `self^-1 mod n` in **variable time**, as a [`Bp512r1ScalarField`] ready for the arithmetic + /// that follows, or `0` for `0`. For ECDSA verification's `s` (FIPS 186-5 §6.4.2 step 4), + /// which is part of the signature and so already public; nothing derived from a private key + /// or a per-message secret may reach this, which is why it lives on the public scalar type + /// and not on [`Bp512r1ScalarField`], whose `invert` is the constant-time one. See + /// [`crate::inverse_vartime`]. + pub fn invert_vartime(&self) -> Bp512r1ScalarField { + Bp512r1ScalarField::from_limbs(crate::inverse_vartime::mod_inverse(&self.0, &N_LIMBS)) + } +} diff --git a/crypto/ec/src/bp512r1_sec1.rs b/crypto/ec/src/bp512r1_sec1.rs new file mode 100644 index 000000000..3438cc5ac --- /dev/null +++ b/crypto/ec/src/bp512r1_sec1.rs @@ -0,0 +1,187 @@ +//! SEC 1 v2.0 (Certicom, May 2009) §2.3.3/§2.3.4/§2.3.5/§2.3.7 point encoding, decoding, and octet- +//! string encoding for brainpoolP512r1. Identical in shape to [`crate::bp256r1_sec1`] -- see that +//! module's docs for the full reasoning (SEC 1 §2.3.3/§2.3.4 cases, why there is no +//! point-at-infinity case, why partial validation is full validation here since RFC 5639 §3.4 +//! gives brainpoolP512r1 cofactor `h = 1`, and the compressed-decode square-root derivation) -- +//! with the curve equation `y^2 = x^3 + Ax + B` (a general `A`, per [`crate::bp512r1_point`], +//! instead of `a = -3`) and brainpoolP512r1's own constants (also `p ≡ 3 (mod 4)`, verified the +//! same way in this module's tests; RFC 5639 §3.4 gives both `p` and `h`). +//! +//! Field elements and scalars are encoded big-endian, fixed-width (§2.3.7's plain big-endian +//! integer encoding) -- the opposite byte order from this crate's internal little-endian `u64` +//! limbs, so [`limbs_from_be_bytes`] and [`be_bytes_from_limbs`] convert at the boundary. + +use crate::bp512r1::{Bp512r1FieldElement, P_LIMBS}; +use crate::bp512r1_domain::{A_LIMBS, B_LIMBS}; +use crate::bp512r1_point::Bp512r1JacobianPoint; +use crate::nat; + +/// `(p + 1) / 4`, the exponent [`decode`]'s compressed-point square root raises `α` to. +const SQRT_EXPONENT: [u64; 8] = [ + 0x4a2a9815960e923d, 0x8a207fcbcb60b1a1, 0xabb3684ab9a8e039, 0x5f5366c026f19a10, + 0xb598e7329c0cc21c, 0xf2cc236cecf27483, 0xcff539ab8cf27f01, 0x2ab7676e36fa7122, +]; + +/// Converts 64 big-endian bytes to little-endian `u64` limbs. +pub fn limbs_from_be_bytes(bytes: &[u8; 64]) -> [u64; 8] { + let mut limbs = [0u64; 8]; + for i in 0..8 { + limbs[i] = u64::from_be_bytes(bytes[(56 - i * 8)..(64 - i * 8)].try_into().unwrap()); + } + limbs +} + +/// Converts little-endian `u64` limbs to 64 big-endian bytes. +pub fn be_bytes_from_limbs(limbs: &[u64; 8]) -> [u8; 64] { + let mut out = [0u8; 64]; + for i in 0..8 { + out[(56 - i * 8)..(64 - i * 8)].copy_from_slice(&limbs[i].to_be_bytes()); + } + out +} + +fn limbs_less_than_p(limbs: &[u64; 8]) -> bool { + let (_, borrow) = nat::sub(limbs, &P_LIMBS); + borrow == 1 +} + +/// `true` iff either coordinate is outside `[0, p)`. +fn either_out_of_range(x_limbs: &[u64; 8], y_limbs: &[u64; 8]) -> bool { + !limbs_less_than_p(x_limbs) || !limbs_less_than_p(y_limbs) +} + +/// `y^2 == x^3 + Ax + B`. +fn is_on_curve(x: &Bp512r1FieldElement, y: &Bp512r1FieldElement) -> bool { + let lhs = y.mul(y); + let x_cubed = x.mul(x).mul(x); + let ax = Bp512r1FieldElement::from_limbs(A_LIMBS).mul(x); + let rhs = x_cubed.add(&ax).add(&Bp512r1FieldElement::from_limbs(B_LIMBS)); + lhs == rhs +} + +/// `base^SQRT_EXPONENT mod p`. Plain (not masked) square-and-multiply: only ever runs on public +/// data (an incoming encoded point), like [`crate::p256_sec1::pow_public`]. +fn pow_public(base: &Bp512r1FieldElement, exponent_limbs: &[u64; 8]) -> Bp512r1FieldElement { + let mut result = Bp512r1FieldElement::ONE; + for limb_idx in (0..8).rev() { + let limb = exponent_limbs[limb_idx]; + for bit in (0..64).rev() { + result = result.square(); + if (limb >> bit) & 1 == 1 { + result = result.mul(base); + } + } + } + result +} + +/// SEC 1 §2.3.3 case 3: `04 || X || Y`, 129 bytes. +pub fn encode_uncompressed(x: &Bp512r1FieldElement, y: &Bp512r1FieldElement) -> [u8; 129] { + let mut out = [0u8; 129]; + out[0] = 0x04; + out[1..65].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out[65..129].copy_from_slice(&be_bytes_from_limbs(&y.to_limbs())); + out +} + +/// SEC 1 §2.3.3 case 2: `02 || X` or `03 || X`, 65 bytes, tag selected by `y mod 2`. +pub fn encode_compressed(x: &Bp512r1FieldElement, y: &Bp512r1FieldElement) -> [u8; 65] { + let mut out = [0u8; 65]; + out[0] = if y.to_limbs()[0] & 1 == 0 { 0x02 } else { 0x03 }; + out[1..65].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out +} + +/// Decodes and validates a SEC 1 §2.3.4 point encoding: 65 bytes (compressed) or 129 bytes +/// (uncompressed). See [`crate::p256_sec1::decode`]'s docs for exactly which checks this performs. +pub fn decode(bytes: &[u8]) -> Option<(Bp512r1FieldElement, Bp512r1FieldElement)> { + match bytes.len() { + 65 => { + let y_tilde: u64 = match bytes[0] { + 0x02 => 0, + 0x03 => 1, + _ => return None, + }; + let x_bytes: [u8; 64] = bytes[1..65].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + if !limbs_less_than_p(&x_limbs) { + return None; + } + let x = Bp512r1FieldElement::from_limbs(x_limbs); + let x_cubed = x.mul(&x).mul(&x); + let ax = Bp512r1FieldElement::from_limbs(A_LIMBS).mul(&x); + let alpha = x_cubed.add(&ax).add(&Bp512r1FieldElement::from_limbs(B_LIMBS)); + let beta = pow_public(&alpha, &SQRT_EXPONENT); + if beta.mul(&beta) != alpha { + return None; + } + let beta_parity = beta.to_limbs()[0] & 1; + let y = if beta_parity == y_tilde { beta } else { beta.negate() }; + Some((x, y)) + } + 129 => { + if bytes[0] != 0x04 { + return None; + } + let x_bytes: [u8; 64] = bytes[1..65].try_into().ok()?; + let y_bytes: [u8; 64] = bytes[65..129].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + let y_limbs = limbs_from_be_bytes(&y_bytes); + if either_out_of_range(&x_limbs, &y_limbs) { + return None; + } + let x = Bp512r1FieldElement::from_limbs(x_limbs); + let y = Bp512r1FieldElement::from_limbs(y_limbs); + if !is_on_curve(&x, &y) { + return None; + } + Some((x, y)) + } + _ => None, + } +} + +/// Decodes and validates an encoded public key (see [`decode`]) directly into a +/// [`Bp512r1JacobianPoint`]. +pub fn decode_point(bytes: &[u8]) -> Option { + let (x, y) = decode(bytes)?; + Some(Bp512r1JacobianPoint::from_affine(x, y)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn p_is_3_mod_4() { + assert_eq!(P_LIMBS[0] & 0b11, 3); + } + + #[test] + fn limbs_less_than_p_boundary_cases() { + assert!(limbs_less_than_p(&[0, 0, 0, 0, 0, 0, 0, 0])); + + let mut p_minus_1 = P_LIMBS; + p_minus_1[0] -= 1; + assert!(limbs_less_than_p(&p_minus_1)); + + assert!(!limbs_less_than_p(&P_LIMBS)); + + let mut p_plus_1 = P_LIMBS; + p_plus_1[0] += 1; + assert!(!limbs_less_than_p(&p_plus_1)); + + assert!(!limbs_less_than_p(&[u64::MAX; 8])); + } + + #[test] + fn either_out_of_range_requires_only_one_coordinate_to_be_out_of_range() { + let in_range = [0u64; 8]; + let out_of_range = P_LIMBS; + + assert!(!either_out_of_range(&in_range, &in_range)); + assert!(either_out_of_range(&out_of_range, &in_range)); + assert!(either_out_of_range(&in_range, &out_of_range)); + assert!(either_out_of_range(&out_of_range, &out_of_range)); + } +} diff --git a/crypto/ec/src/bp512r1_wnaf.rs b/crypto/ec/src/bp512r1_wnaf.rs new file mode 100644 index 000000000..563ffc257 --- /dev/null +++ b/crypto/ec/src/bp512r1_wnaf.rs @@ -0,0 +1,148 @@ +//! Interleaved windowed-NAF (wNAF) Shamir's-trick multiplication: `[u]G + [v]Q`, for +//! brainpoolP512r1 ECDSA verification. Identical algorithm to [`crate::p256_wnaf`] -- see that +//! module's docs for the full derivation and verification methodology -- with the limb count +//! swapped for brainpoolP512r1's (`WNAF_LEN = 513`, one more than `n`'s 512-bit length, same +//! margin as P-384's own `385 = 384 + 1`). +//! +//! `G`'s odd multiples are a compile-time affine table (`bp512r1_wnaf_table`, width 7) added by mixed +//! addition; only `Q`'s table is built per call. See [`crate::p256_wnaf`]. + +use crate::bp512r1::Bp512r1FieldElement; +use crate::bp512r1_point::Bp512r1JacobianPoint; +use crate::bp512r1_scalar::Bp512r1PublicScalar; +use crate::bp512r1_wnaf_table::{G_ODD_MULTIPLES_X, G_ODD_MULTIPLES_Y}; +use crate::nat; + +/// Window for `Q`, whose odd multiples are computed per call: 8 point additions to build the +/// table, one addition per ~6 digits to use it. +const WIDTH: usize = 5; +/// Window for `G`, whose odd multiples are a compile-time table +/// ([`G_ODD_MULTIPLES_X`]/[`G_ODD_MULTIPLES_Y`]): nothing to build, so the widest window a +/// signed `i8` digit allows, giving one addition per ~8 digits instead of ~6, and every one of +/// them a mixed addition against an affine entry. +const G_WIDTH: usize = 7; +const WNAF_LEN: usize = 513; +// Mutating this expression (e.g. `WIDTH - 2` -> `+ 2`) only grows the table `odd_multiples` builds +// and `lookup_signed` reads from; the actual digit range `compute_wnaf` can produce is bounded by +// `WIDTH` itself (unchanged), so the extra entries are simply never indexed -- an accepted mutant. +const ODD_MULTIPLE_COUNT: usize = 1 << (WIDTH - 2); + +/// `[u]G + [v]Q`. +pub fn shamir_multiply( + u: &Bp512r1PublicScalar, + v: &Bp512r1PublicScalar, + q: &Bp512r1JacobianPoint, +) -> Bp512r1JacobianPoint { + let du = compute_wnaf(u.to_limbs(), G_WIDTH); + let dv = compute_wnaf(v.to_limbs(), WIDTH); + + let table_q = odd_multiples(q); + + let mut r = Bp512r1JacobianPoint::INFINITY; + for i in (0..WNAF_LEN).rev() { + r = r.double(); + if let Some((x, y)) = lookup_signed_g(du[i]) { + r = r.add_vartime_affine(&x, &y); + } + if let Some(add_q) = lookup_signed(&table_q, dv[i]) { + r = r.add_vartime(&add_q); + } + } + r +} + +fn shr1(limbs: &[u64; 8]) -> [u64; 8] { + // Each `limbs[i] >> 1` only occupies bits 0-62 (its own bit 63 is shifted out), and + // `(limbs[i+1] & 1) << 63` only ever occupies bit 63 -- disjoint, so `|` and `^` agree here. + [ + (limbs[0] >> 1) | ((limbs[1] & 1) << 63), + (limbs[1] >> 1) | ((limbs[2] & 1) << 63), + (limbs[2] >> 1) | ((limbs[3] & 1) << 63), + (limbs[3] >> 1) | ((limbs[4] & 1) << 63), + (limbs[4] >> 1) | ((limbs[5] & 1) << 63), + (limbs[5] >> 1) | ((limbs[6] & 1) << 63), + (limbs[6] >> 1) | ((limbs[7] & 1) << 63), + limbs[7] >> 1, + ] +} + +/// The width-`width` wNAF digits of `k` (`width` at most 7, so a digit fits an `i8`), LSB-first; unused trailing entries are `0`. Not +/// constant time: only ever called on public scalars (see the module docs). +fn compute_wnaf(k_limbs: [u64; 8], width: usize) -> [i8; WNAF_LEN] { + let mut digits = [0i8; WNAF_LEN]; + let mut k = k_limbs; + let mut pos = 0; + while k != [0, 0, 0, 0, 0, 0, 0, 0] { + if k[0] & 1 == 1 { + // Mutating this mask (e.g. `(1 << WIDTH) - 1` -> `+ 1`) is an accepted mutant, not a + // bug: whatever mask is used, `digit` always keeps `k[0]`'s low (odd) bit, so it's + // always odd -- the only invariant `lookup_signed` and the subtract-or-add-then-`shr1` + // step below actually rely on. A degenerate mask just makes this compute a + // non-minimal (but still exactly reconstructing `k`) signed-digit sequence -- see + // `crate::p521_wnaf`'s identical case for the full argument and verification. + let mut digit = (k[0] & ((1 << width) - 1)) as i16; + if digit >= 1 << (width - 1) { + digit -= 1 << width; + } + k = if digit >= 0 { + let (new_k, borrow) = nat::sub(&k, &[digit as u64, 0, 0, 0, 0, 0, 0, 0]); + debug_assert_eq!(borrow, 0); + new_k + } else { + let (new_k, carry) = nat::add(&k, &[(-digit) as u64, 0, 0, 0, 0, 0, 0, 0]); + debug_assert_eq!(carry, 0); + new_k + }; + debug_assert!(pos < WNAF_LEN, "wNAF encoding overflowed its verified bound"); + digits[pos] = digit as i8; + } + k = shr1(&k); + pos += 1; + } + digits +} + +/// The odd multiples `1*p, 3*p, 5*p, ..., (2*ODD_MULTIPLE_COUNT - 1)*p`, indexed so entry `i` +/// holds `(2i+1)*p`. +fn odd_multiples(p: &Bp512r1JacobianPoint) -> [Bp512r1JacobianPoint; ODD_MULTIPLE_COUNT] { + let double_p = p.double(); + let mut table = [*p; ODD_MULTIPLE_COUNT]; + let mut cur = *p; + for entry in table.iter_mut() { + *entry = cur; + cur = cur.add_vartime(&double_p); + } + table +} + +/// `digit * p` where `p` is the point `table` was built from ([`odd_multiples`]), or `None` for +/// digit `0`. +fn lookup_signed( + table: &[Bp512r1JacobianPoint; ODD_MULTIPLE_COUNT], + digit: i8, +) -> Option { + // Every accepted-mutant survivor below relies on `digit` always being odd (it's built from an + // odd `k[0]` in `compute_wnaf`, see that function's docs): `digit > 0` vs `>= 0` agree once + // the `digit == 0` case is already handled above, and for odd `digit`, `(digit - 1) / 2 == + // digit / 2` (and symmetrically for `-digit`), since integer division already floors an odd + // numerator down to the same value subtracting 1 first would. + if digit == 0 { + None + } else if digit > 0 { + Some(table[(digit as usize - 1) / 2]) + } else { + Some(table[(-digit as usize - 1) / 2].negate()) + } +} + +/// `digit * G` for a width-[`G_WIDTH`] digit, read from the compile-time table of odd multiples +/// of `G` as affine coordinates (negating `y` for a negative digit), or `None` for digit `0`. +fn lookup_signed_g(digit: i8) -> Option<(Bp512r1FieldElement, Bp512r1FieldElement)> { + if digit == 0 { + return None; + } + let index = (digit.unsigned_abs() as usize - 1) / 2; + let x = Bp512r1FieldElement::from_limbs(G_ODD_MULTIPLES_X[index]); + let y = Bp512r1FieldElement::from_limbs(G_ODD_MULTIPLES_Y[index]); + Some(if digit > 0 { (x, y) } else { (x, y.negate()) }) +} diff --git a/crypto/ec/src/bp512r1_wnaf_table.rs b/crypto/ec/src/bp512r1_wnaf_table.rs new file mode 100644 index 000000000..fc2500319 --- /dev/null +++ b/crypto/ec/src/bp512r1_wnaf_table.rs @@ -0,0 +1,297 @@ +//! The checked-in odd multiples of the base point `G` for [`crate::bp512r1_wnaf`]'s fixed side: +//! `G_ODD_MULTIPLES_X[i]`/`G_ODD_MULTIPLES_Y[i]` are the affine coordinates of `(2i + 1) G`, for +//! `i` in `0..32`, as little-endian `u64` limbs -- the table a width-7 wNAF over `G` indexes. +//! `Q`'s table is built at run time because `Q` is the signer's key; `G` is the curve's constant, +//! so its table need not be. +//! +//! Generated in Python from the affine group law (SP 800-186 Appendix A.1.1) and RFC 5639 §3.4's `G`, +//! independently of this crate's arithmetic, and checked there by `n * G == infinity`; this +//! module's own test regenerates every entry with the crate's point arithmetic and compares. + +/// `x` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_X: [[u64; 8]; 32] = [ + [ + 0x8b352209bcb9f822, 0x7c6d5047406a5e68, 0x50d1687b93b97d5f, 0xff3b1f78e2d0d48d, + 0xb43b62eef4d0098e, 0x85ed9f70b5d916c1, 0x5a21322e9c4c6a93, 0x81aee4bdd82ed964, + ], + [ + 0x5d4abf882ccb8d94, 0xf56e34abfa9ac720, 0x4780ae53e1853d62, 0xc1f6fb975ceecade, + 0x09c09cefd830151b, 0x907c80ef3bc24593, 0x36cdd42543f20afe, 0x08dd87e12b0a4cc4, + ], + [ + 0x500d6f7d9d9aaa5c, 0x17bc4f43d413540b, 0xc393c273727cf25d, 0xaadc73e8d9472bb0, + 0xae0ff1c9461693d2, 0x0a4abf8dd044a3c1, 0xe3c9bc8c2bf17781, 0x8672838ed83a55b9, + ], + [ + 0xbecedb316e1d30fa, 0xb55b556c926f2a3d, 0xc1df5ccd7685cf85, 0x8ee59581616f83f9, + 0x40ce49b6a78fc195, 0x3bcb810dfd2826b5, 0x783b62c3ef7f473c, 0x74a4bbc7d1a22ea7, + ], + [ + 0x973c40b0fbd92c64, 0xa0bcaae472b75daa, 0x71d60de7d7a6fb36, 0x4492eb78493d9671, + 0xd9cd604c8aac2275, 0xdcc04fd8188cbfdf, 0xcfe49337d863265d, 0x595b061e1aa360e8, + ], + [ + 0xa1eb911c7353e301, 0x9e29656152501da4, 0x289f56a5eee69428, 0x12082c66a9ef7c94, + 0x780f003411642093, 0xa0f3f87b3053606d, 0xe5359be155a58d80, 0x9018d91a9ef584e5, + ], + [ + 0x9fed7e841e9a3ef1, 0x74c799aa8c04c090, 0xb67b5b9c96c4e3ad, 0x17f711874b8f5ec2, + 0x5c371d5c62e68e9a, 0x9aea9422f84b88dc, 0x5c38e8e42f1993c8, 0x661dafd56964ecae, + ], + [ + 0x66b47ddf2d5721fe, 0xd9a221da4d35ce21, 0x50fdea5ecd222628, 0x1006bca2508be5bc, + 0x21f05845db650dbc, 0x864f46510c00c7a5, 0xcab2c7b55991b47f, 0x77bea740013fc519, + ], + [ + 0x4633f02af2e5904e, 0x82b69de20e152e71, 0x2e893e5ef66f8f39, 0x86c02d4b2e082217, + 0x6ae78e6f6e3c6e2d, 0x56f7c50386366654, 0x71e963451a74e855, 0x7e8ec7d1a5bc63e3, + ], + [ + 0xac9bf8a861653328, 0x24f8d90a4cad66b6, 0xcf40e4483230c621, 0x75ba5ae7e60f1de7, + 0x649e56cbf76666d1, 0x1e6f73a9e0de7706, 0xf1bc383341fcb4f4, 0x5fa0c675339433e1, + ], + [ + 0x303626d1fa158c1e, 0x656079973d1ed6e5, 0x1bbd991b1862dac6, 0x4a0217dceebfa7d7, + 0xda130d566e059b61, 0x105f057fe5a4578f, 0xdf516dc53e0c9ad2, 0x89c3f72f8d12832e, + ], + [ + 0x969d49e509075319, 0x6c501dd070b6d60e, 0xe98af9cac2f0788d, 0x01d8b0541d51f710, + 0xce49a22c22aa773b, 0x17eae42154dc622e, 0x02a03155a9fa145d, 0xa2016a80603e2f70, + ], + [ + 0x9463bad3b32f7e84, 0x4498fdfdf738d903, 0x77a018be9f77f288, 0xaec2367dad248030, + 0xc54491ac984bb270, 0xae05b12c2a2d6910, 0xd857cf8a5d858d7f, 0x6bcd80cf6403d095, + ], + [ + 0x211200d41ef2dbd1, 0x4d421be9021efcdf, 0xb21f06dfcd5affef, 0x10dd934b5ed331d4, + 0x870f281c8f869afe, 0xe3e1e88b70c89c47, 0xf1f5662b66ed8e9c, 0x29ce5366e6aa2bbe, + ], + [ + 0x505b17e3aafb1c73, 0xfa33db5607a037f3, 0x4fa488cab8d11be2, 0xea1d3dbb87674eb5, + 0x102b4e800cc433ee, 0x4ffe1d1c71d7b90b, 0x759af1f7c28eda71, 0x8b1cadae1645d48e, + ], + [ + 0x7d126bf535d8db28, 0x53c8a025b81eebe7, 0xbb0a16b0b0f57294, 0x83de23fc3efbb330, + 0x81814d53ccf861ef, 0x1eb5a6221df34f16, 0x0b831add9a5aca78, 0x017f2c8f9c76e54c, + ], + [ + 0x102154e35b6f0e1f, 0x1bfb7ba06ba1b048, 0xdccb2d2d0460bdde, 0x5c964b8f4bef47cd, + 0xb8d1a28e7f4dc306, 0x6277ed0057de7fe3, 0xfee290555f0b13e6, 0x79b00304cce8b7f3, + ], + [ + 0x3f5cb20ada9456b8, 0x1d13ca2c631a3797, 0x7a2a3b649a0aaa68, 0x04c3b7c624243ba1, + 0x18cb4ef474808b81, 0xd2c21628441536be, 0xa2e91b0d421ac1da, 0x10a4c8f9f50fd4a0, + ], + [ + 0x1b24a1c56fe1941a, 0xcdf29dcaa4572c44, 0x9340ced846de478a, 0x4da8f41d415d4934, + 0x9f25446b6cb4de22, 0xb62a0cb22a4c6ecb, 0xd5200a3dc99cfb9a, 0x158186e7d0ac1874, + ], + [ + 0x747d2ec171a65853, 0x2462ab88f85d3912, 0xf7d0597d7d47e5f4, 0x01fe17dd3d4bff64, + 0x4b2b2244cc0a404a, 0x45d54199b7786f50, 0x762388a0f9f1a184, 0x94bfaf82cc18a657, + ], + [ + 0x22ac7c6e28a0492f, 0xb0b27b6bf0b0c5c9, 0x66d8e34befaf2736, 0x569429b0e8fbdc16, + 0xc189e5da5396294f, 0x562c6ba7eacc934d, 0x319409a4b3a9d13f, 0x7a8b9d1e1aa42de1, + ], + [ + 0x4e262fb1e9df618f, 0xf61b0c2874a6f96f, 0x0fdf9d3ca7d5a03b, 0x67fb947628145c0f, + 0x6c31be69f57b1bde, 0x849022b8a03cc33b, 0xf8104510faac810a, 0x058fa41066f8cd35, + ], + [ + 0x890b29dbd092ee23, 0x6a272ebaedb8ee70, 0xbb2d16948e0181e6, 0xac7af3e7b5b668c6, + 0x43d48e5c8361d46f, 0x4df5023131ec940a, 0x92618b52ecd754f9, 0x2b7c188fa7ff9fb6, + ], + [ + 0xda76b7936242c769, 0xbe4983fb4ffb491e, 0x5b75d82c9c21ded1, 0x89303f79fde816bf, + 0x93e41685754c71cc, 0x329574b737bb1dd8, 0xc3effb75ddf74b56, 0x9e0a451f50d1fb25, + ], + [ + 0xd65193332300ed9b, 0x5d8489b59ee166fb, 0xd01950844e867ecc, 0x82716249a41e4af3, + 0x8c458e673597c950, 0x5218af3541b88884, 0xe59d65779dd0a4ef, 0x8d001732b8386361, + ], + [ + 0x63c419b19c42dbb4, 0x4f9d981e0dc5edbe, 0x9ac5d8ef17158db8, 0x47b1fbcb4e6143f9, + 0xd48970aa9b7fef47, 0x6fdd83e67c5536b5, 0x098ead28a40f14f8, 0x7a4d965c0391815a, + ], + [ + 0x5b35e0346f98e025, 0x3531acd7b4ee413c, 0x78c512947909ef05, 0x903cbcfe3524e59a, + 0x19a5ccf6d3f997fa, 0x91a0af321de97712, 0xfae3059f30a459e2, 0x81f7f38b0ca3022a, + ], + [ + 0x3090557814c9e668, 0x5cbe71832d62452d, 0xe05386f839803196, 0x4e27d3216cb59599, + 0x038e3d801d352e87, 0x7b82a9046adca6c8, 0xede44e0fdcfbb301, 0x6290831bdedf66b0, + ], + [ + 0x16eb282f0e26f3ea, 0xe6e83ad8fac91978, 0xa50b0c06131c394e, 0x09ee51315f805928, + 0xdd063a439a29a40c, 0x0fb4b082a2801a19, 0xf559c8b5fda613d1, 0x713b2f6c827e5b15, + ], + [ + 0xdf9b5de1c78b84ac, 0x4235623822b16814, 0x375c37732584ffc2, 0x43bd0c1ac08d7801, + 0xf91d21e6dcf1f344, 0xc17d7aba2e600760, 0x9c2c8508cdd8e504, 0x6650e7aeb003aeb2, + ], + [ + 0xfaf6c30996ba2b5b, 0x63b5301ecca640cf, 0xdd8ee4fd6fabf753, 0xe67639bf11913dfd, + 0xeeb77a9533ea933e, 0x3f40596c341f5630, 0x60bce0116590a8eb, 0x0780ce8523d0ae7a, + ], + [ + 0x38638c9cce7ffc55, 0xa9322c197da0e86b, 0xe8d61ca0ec012213, 0x115a137415fabf7f, + 0x07b77747baf33081, 0x9c5d4c012d3ceef9, 0xb96e1c8629f8c07f, 0x5156adced9997afb, + ], +]; + +/// `y` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_Y: [[u64; 8]; 32] = [ + [ + 0x78cd1e0f3ad80892, 0xd1ca2b2fa8f05406, 0x5bca4bd88a2763ae, 0xb2dcde494a5f485e, + 0xa000c55b881f8111, 0xf209f70024a57b1a, 0xc0eabfa9cf7822fd, 0x7dde385d566332ec, + ], + [ + 0x5cdeafba05b02c37, 0x8cce5bc75d8de649, 0xfebfcb69c0f37c5f, 0xacdab8eb772327b3, + 0xba0b382e1716d843, 0x43d903b4a6334c4b, 0x756ff0067376fa75, 0x026ef5c6e1dab71d, + ], + [ + 0x62f8a1c2b51f7f35, 0xdb58bb174dcc0c77, 0xfa11d3cc5b06c0df, 0xa5a88c91f28d09eb, + 0xe1b69903a8863d2f, 0xea9d3a0a7a668f1e, 0x52b7a5643c936c09, 0x151d93c1de2ed9ee, + ], + [ + 0x1b031a2415c8761e, 0xdbb183a1fff72bda, 0xe9a929aead052089, 0x007dbc9e60fd30af, + 0xe68c55af183e3430, 0xb7a2e873ce087973, 0x17495d11b106ae2b, 0x73d3c07ba1bf2908, + ], + [ + 0x904d51202dd6a26c, 0x0ac0cd086e05b92f, 0xd2a2cb6fa4d26a1e, 0x44b2058b82bf9cab, + 0x88a0a9fbde5bda3c, 0x611a61ccb943a004, 0x2a410f997aa69880, 0x6a3fe9031a5472af, + ], + [ + 0xea4749c79c5f39b2, 0xd134521b4ff7d351, 0xfb571e45527fdf79, 0x06cc328709eefba9, + 0x26709006a1dfaac0, 0xcb5d48a02b86241f, 0xf9e48f7c68dd11bf, 0x30b56337977ca5c1, + ], + [ + 0xf250dc39c61c556c, 0x4d84dcea90a1f58d, 0x19b39c3eb5447e71, 0x768ff6c863f41a25, + 0x151618f233bdfc03, 0x567c143080f6b686, 0xb3088f186faa284e, 0x91c294bcca5a4acc, + ], + [ + 0x49a4fe7242c58aa1, 0x33ab304fadbbe334, 0x41a4c84c80e3d9d9, 0xd2a9885975db218f, + 0x2de9aecd4db65dbe, 0x9eb524baa6808c61, 0x273c6f76e838e4f9, 0x15b48320c1a04c2a, + ], + [ + 0xfb4118e0663c185d, 0xc7dc76af0beec3fe, 0xcd19de87b4ea3dc2, 0x4b8aa9c481570d9e, + 0x0a1ea8b7bda4434a, 0x95fecb9a53add1f7, 0xa087ada1fe365cb8, 0x06b2a67269285258, + ], + [ + 0xda4f480a2b2e1414, 0x9f3a16da9ddcc559, 0x11544dd5031a227f, 0xa097b83f9e611903, + 0xc9d7f4a3e973fb44, 0x19ec9382352b2300, 0x39ca172334288e78, 0x7205c29f436390ce, + ], + [ + 0x8a6e017443af9cc0, 0x7157dc468b7b961d, 0x3a2b978fabf13a26, 0x414918db498cf95d, + 0xa447acde5d328385, 0x0722a0e1d71413b4, 0xd60b3ddb4f8e26d4, 0x085a88a71275a7f5, + ], + [ + 0x4bc4e04766b2d6dc, 0xa880e625ee4801aa, 0xbb12805a9cc2190d, 0xf709c3906dc9cad0, + 0xd588a15f9962ff5c, 0x519a0b14d8f0670b, 0x311214f093546346, 0x757702e3f163c8f7, + ], + [ + 0x16358adec9825713, 0x883589b5f1de4cc3, 0x88f5ea39f5b43f5c, 0xdba9cbb60c559ed7, + 0x9444d62a11cf9e7d, 0x29af23a230b91ed0, 0xc9e558cc02703ca1, 0x325e3b5a446a0a8c, + ], + [ + 0xe94bd5b1d3137aa0, 0x69c623628317df8f, 0x3bc260ab8312f776, 0x42f49c623f3772bf, + 0xa8f6677f60f56943, 0xfd1c0f1b72c56679, 0x43e9082618c23fe6, 0x6432b5cd342338d3, + ], + [ + 0x76ddf6df23e3022b, 0x359c1249a81525bc, 0xfd0dc499fb4bc509, 0xe6a16cbfd9c0572b, + 0xe9aae4b69e6ad24c, 0xcc77edf608a25dbe, 0x78f7dd40cf18146b, 0xa89818ba6a6494ac, + ], + [ + 0x1d7d8c426fbc0514, 0x6a61f9798fda995e, 0xc0d413c1d1327a76, 0x8a3d024cb84b2fa4, + 0xee75fcbc79a5a85a, 0x78966d2f86f1c2fb, 0x690f0656e2e6cd53, 0x3229e04fd2dfd0da, + ], + [ + 0x45151d9fa02df8ea, 0x82335cb555322db3, 0x867c89a3134daee6, 0x041ee264ea8923fe, + 0x1e1bf782a33410b3, 0xaff0badfe0b00077, 0xa8b0f225c99c6cde, 0x64b23a99f243c935, + ], + [ + 0x2ab0b85f36acfe52, 0x0f3ca4fd7d1e64ee, 0x193b47e6b481d6d3, 0xea807b48720ec5f2, + 0xd4997da426b66a5e, 0x88d3d03b7bb10c17, 0xdcef5568bd74d15e, 0x9db8016e59ea3516, + ], + [ + 0x7e7fbf680ec23f31, 0x3024346db8337e9d, 0x55c9eded125dc470, 0x0ed224e77f5e6466, + 0x031501550e4aac3f, 0xa17f0e219bc671f9, 0x27a3bbda7b6f916d, 0x3865aaca06756578, + ], + [ + 0x5a11320a12bf40d9, 0x6c4705c545c09fb8, 0xb6603d2ba3307cd9, 0xeb6b3c521ba039ab, + 0xaea8b174d0957ff7, 0x20852aceceddae30, 0x03dc2a22ab90d72c, 0x077e7de8dbf8bf1d, + ], + [ + 0xb9ec5c2c63ee15c6, 0x903999f8161cfcc3, 0xfe9a9d8dc8aebe6d, 0x529faa430bffbd64, + 0x2f35412b19a54192, 0xbeb4d71ce395cb43, 0x797409d1001c3cd7, 0x798b61785b623b5e, + ], + [ + 0x4afecb12fefb01ac, 0x1e6e9069597db7b4, 0xa9707e4b41802457, 0x3d62e8e3e94cd132, + 0x9084da1c25eeec03, 0xd17b6ab1816f5d47, 0xfa787156d1a865bf, 0x8536ac25f5b9801a, + ], + [ + 0xc23e0d1131523bee, 0x5db90b4d2dab3c39, 0x12bdee37e1933b29, 0x24a81c1d000e7e7a, + 0x9644ab761851400f, 0x254288c6adbbe4e5, 0xd2333add90575470, 0x270b58fd8ddb2447, + ], + [ + 0x0f5e450a34e71f8b, 0x744e1a3d1ee317f0, 0xd71c96423f610a6e, 0x78506947f64386c4, + 0x73873d4efc817278, 0xc3d828b25e8f1da4, 0xa4ddc128da101684, 0x04fb93ebaa1c02fb, + ], + [ + 0x1c72bf3812e95f21, 0xc9eab9679727356a, 0x322a0235d7b36071, 0x6587663c2e1a8c06, + 0xd4ef9bb50628b1ab, 0xee15c736267745a7, 0xfcaa2834de84f935, 0x66691b2aae64991b, + ], + [ + 0xe0b81dabc7a99b97, 0xc51251dcdbec2702, 0x46f1457afdf97344, 0xc8779f0997ee9f34, + 0x2681bf58d93aed44, 0xd9bb1eea95b8b1a4, 0x7eeb72c5d46f8fd6, 0x4cf57b66f4a98326, + ], + [ + 0x8967560bc3c2fd7d, 0x06828c4b80608cc7, 0x2f934cfccabb7fc8, 0xadd93a2304b79344, + 0x3f3093f65168efd8, 0xcf4093bd36146e00, 0x2ef0cc858b14d3d3, 0x1f578dc4ddb66dfd, + ], + [ + 0x587e9e9a66632d34, 0x292544a030d592cd, 0xe8b0ef01d22abefb, 0xff90df5d283eec02, + 0x66c89fa311c41617, 0x0b61a9e0807ad00f, 0x78893bf6bef530e5, 0x513613bdc1ff37d4, + ], + [ + 0x6b3f4b8c0b1f6ee1, 0xa2159c852bffce0b, 0x9bc839e3fb2e22f8, 0x39d20cea0256cd91, + 0x77fabc576ba41582, 0x2cfb90f5fffd8ab3, 0x51c05946dc87171d, 0xaa6524356cff292f, + ], + [ + 0x741114bf7442c847, 0x2789116bc42351dd, 0x90e5ca7d8c3b7d7a, 0x018f871d527055a2, + 0x90af1aec4bb109f1, 0xd29e7d46744616b4, 0x36221d3e19998454, 0x26884506891976e2, + ], + [ + 0x2ac11915752a463e, 0x4651fcf9c9ffeb58, 0x9ce8970604326060, 0x591b0982c65653bc, + 0xa96ed6c7fb75192b, 0x9ca69b4d5af033e8, 0x1e1ca93515609b39, 0x7f238102bf71dfba, + ], + [ + 0x8add896b45b74a66, 0xbd57fb04a5550de0, 0x72310307d27ce1e1, 0xd4eec5d101929d96, + 0x23b6ab98106bd881, 0x5421eea261fc3d87, 0xb5a0549c79b7ca09, 0x1eeb61365df768bf, + ], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::bp512r1::Bp512r1FieldElement; + use crate::bp512r1_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::bp512r1_point::Bp512r1JacobianPoint; + + #[test] + fn table_matches_the_odd_multiples_of_g_computed_by_the_point_arithmetic() { + let g = Bp512r1JacobianPoint::from_affine( + Bp512r1FieldElement::from_limbs(G_X_LIMBS), + Bp512r1FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + let mut cur = g; + for i in 0..32 { + let (x, y) = cur.to_affine().expect("an odd multiple of G below n is never infinity"); + assert_eq!(x.to_limbs(), G_ODD_MULTIPLES_X[i], "entry {i}: x of {}G", 2 * i + 1); + assert_eq!(y.to_limbs(), G_ODD_MULTIPLES_Y[i], "entry {i}: y of {}G", 2 * i + 1); + cur = cur.add_vartime(&two_g); + } + } +} diff --git a/crypto/ec/src/inverse_vartime.rs b/crypto/ec/src/inverse_vartime.rs new file mode 100644 index 000000000..26d90ea1b --- /dev/null +++ b/crypto/ec/src/inverse_vartime.rs @@ -0,0 +1,117 @@ +//! Variable-time modular inversion for **public** values: the binary extended Euclidean algorithm +//! (Hankerson, Menezes & Vanstone, *Guide to Elliptic Curve Cryptography*, Algorithm 2.22, +//! "Binary algorithm for inversion in F_p"), generic over the limb count `L` like [`crate::nat`]. +//! +//! Every curve's `ScalarField::invert` is a fixed square-and-multiply over the public exponent +//! `n - 2`: the same ~`1.5 * bits` field multiplications whatever the input, which is what a +//! secret `k^-1` needs. ECDSA verification also inverts a scalar, `s` (FIPS 186-5 §6.4.2 step 4), +//! and `s` is part of the signature: it is known to anyone who holds the signature, so nothing +//! about it is protected by taking constant time over it. This algorithm branches and +//! shifts on its operands freely and finishes in a small multiple of `bits` single-limb +//! operations rather than hundreds of full multiplications; measured on P-256, that is a +//! several-fold speedup of the inversion and a few percent of a whole verification. +//! +//! It is reached only through each curve's `PublicScalar::invert_vartime`, so the type system -- +//! not a reviewer -- keeps a secret `Scalar` away from it, the same way it keeps one away from +//! the variable-time point multiplier. +//! +//! # The algorithm +//! +//! Algorithm 2.22, for an odd modulus `n` and `0 < a < n` coprime to it: +//! +//! ```text +//! u <- a, v <- n, x1 <- 1, x2 <- 0 +//! while u != 1 and v != 1: +//! while u is even: u <- u/2; x1 <- x1/2 if x1 is even, else (x1 + n)/2 +//! while v is even: v <- v/2; x2 <- x2/2 if x2 is even, else (x2 + n)/2 +//! if u >= v: u <- u - v, x1 <- x1 - x2 (mod n) +//! else: v <- v - u, x2 <- x2 - x1 (mod n) +//! return x1 if u == 1, else x2 +//! ``` +//! +//! `u` and `v` only ever shrink, so both stay below `n` and fit in `L` limbs; `x1` and `x2` are +//! kept reduced mod `n`, so the only intermediate that can exceed `L` limbs is `x + n` before its +//! halving, whose carry bit [`shr1_with_carry`] folds back in. `a = 0` has no inverse and returns +//! `0`, matching the constant-time `invert`'s convention, so callers can substitute one for the +//! other without a special case. + +use crate::nat; + +/// `a^-1 mod n` for odd `n` and `a < n`, or `0` when `a == 0`, in variable time. See the module +/// docs for what "public values only" means here and why. +pub fn mod_inverse(a: &[u64; L], n: &[u64; L]) -> [u64; L] { + debug_assert!(n[0] & 1 == 1, "mod_inverse needs an odd modulus"); + debug_assert_eq!(nat::sub(a, n).1, 1, "mod_inverse needs a < n"); + + if nat::is_zero(a).to_bool() { + return [0u64; L]; + } + + let mut one = [0u64; L]; + one[0] = 1; + + let mut u = *a; + let mut v = *n; + let mut x1 = one; + let mut x2 = [0u64; L]; + + while u != one && v != one { + // Mutating this parity test to `u[0] | 1 == 0` (never halve `u`) is an accepted equivalent + // mutant, not a gap: the invariant `u == x1 * a (mod n)` holds regardless of whether `u` + // is halved, and the `v` side and the subtractions alone still drive `u` or `v` down to + // `1` -- it is the subtractive Euclidean algorithm, slower but correct. (Mutating it to + // `^`, which halves an odd `u` whose low limb is exactly `1`, breaks the invariant and is + // caught: the tests feed `2^64 + 1`-shaped inputs for exactly that reason.) + while u[0] & 1 == 0 { + u = shr1_with_carry(&u, 0); + x1 = halve_mod_n(&x1, n); + } + while v[0] & 1 == 0 { + v = shr1_with_carry(&v, 0); + x2 = halve_mod_n(&x2, n); + } + // `u >= v` iff `u - v` does not borrow. + let (u_minus_v, borrow) = nat::sub(&u, &v); + if borrow == 0 { + u = u_minus_v; + x1 = sub_mod_n(&x1, &x2, n); + } else { + v = nat::sub(&v, &u).0; + x2 = sub_mod_n(&x2, &x1, n); + } + } + + if u == one { x1 } else { x2 } +} + +/// `(value + carry * 2^(64L)) >> 1`: a right shift by one bit over `L` limbs, with `carry` (0 or +/// 1) entering as the new top bit. +fn shr1_with_carry(value: &[u64; L], carry: u64) -> [u64; L] { + let mut out = [0u64; L]; + for i in 0..L { + let high = if i + 1 < L { value[i + 1] } else { carry }; + // `value[i] >> 1` leaves bit 63 clear and `high << 63` occupies only bit 63, so the two + // operands are disjoint: mutating this `|` to `^` is an accepted equivalent, not a bug -- + // the same disjoint-OR case the crate's `widening_square` and `p521::reduce` document. + out[i] = (value[i] >> 1) | (high << 63); + } + out +} + +/// `x / 2 mod n` for odd `n` and `x < n`: `x >> 1` when `x` is even, else `(x + n) >> 1`. The +/// sum `x + n` is below `2n < 2^(64L+1)`, so its single possible carry bit is what +/// [`shr1_with_carry`] takes. +fn halve_mod_n(x: &[u64; L], n: &[u64; L]) -> [u64; L] { + if x[0] & 1 == 0 { + shr1_with_carry(x, 0) + } else { + let (sum, carry) = nat::add(x, n); + shr1_with_carry(&sum, carry) + } +} + +/// `a - b mod n` for `a, b < n`: the wrapped difference, plus `n` if it borrowed. +fn sub_mod_n(a: &[u64; L], b: &[u64; L], n: &[u64; L]) -> [u64; L] { + let (diff, borrow) = nat::sub(a, b); + if borrow == 1 { nat::add(&diff, n).0 } else { diff } +} diff --git a/crypto/ec/src/lib.rs b/crypto/ec/src/lib.rs new file mode 100644 index 000000000..5a0cdbae2 --- /dev/null +++ b/crypto/ec/src/lib.rs @@ -0,0 +1,252 @@ +//! Constant-time elliptic-curve arithmetic over compile-time-selected Weierstrass curves. +//! +//! This crate is the "custom curve" math substrate: fixed-width limb arithmetic ([`nat`]) and, +//! per curve, a base-field implementation with a reduction tailored to that curve's prime. +//! +//! It deliberately does not support a runtime-described generic curve, and that is a +//! constant-time decision rather than a scope one. Field arithmetic on a secret value has to run +//! in the same time whatever the value is, which needs limbs of a fixed width known at compile +//! time; a `BigInteger`-shaped value leaks the magnitude of a secret through the length of its own +//! internal representation, quite apart from its arithmetic being variable-time. So the custom +//! path is not an optimisation here -- it is the only path on which constant-time signing is +//! achievable at all. The cost, accepted deliberately, is that curves are types chosen at compile +//! time and the supported set is fixed by what is implemented. +//! +//! This crate's custom-curve arithmetic, and the Brainpool and SM2 curves alongside it, are based +//! on BC Java's (`org.bouncycastle.math.ec.custom`) equivalent per-curve implementations. +//! +//! # Usage Examples +//! +//! Most callers want [`bouncycastle_ecdsa`](../bouncycastle_ecdsa/index.html) or +//! [`bouncycastle_sm2`](../bouncycastle_sm2/index.html) rather than this crate directly; what +//! follows is the layer underneath them. +//! +//! Field arithmetic, per curve: +//! +//! ``` +//! use bouncycastle_ec::p256::P256FieldElement; +//! +//! let a = P256FieldElement::from_limbs([7, 0, 0, 0]); +//! let b = P256FieldElement::from_limbs([3, 0, 0, 0]); +//! +//! assert_eq!(a.add(&b), P256FieldElement::from_limbs([10, 0, 0, 0])); +//! assert_eq!(a.sub(&b), P256FieldElement::from_limbs([4, 0, 0, 0])); +//! assert_eq!(a.mul(&b), P256FieldElement::from_limbs([21, 0, 0, 0])); +//! assert_eq!(a.square(), a.mul(&a)); +//! assert_eq!(a.mul(&a.invert()), P256FieldElement::ONE); +//! ``` +//! +//! `[k]G` for a secret scalar, the operation signing is built on. The scalar type is +//! [`p256_scalar::P256Scalar`], which wraps its value in +//! [`bouncycastle_utils::secret::Secret`]; the multiplier is branch-free in `k`: +//! +//! ``` +//! use bouncycastle_ec::p256_comb::comb_multiply_base_point; +//! use bouncycastle_ec::p256_scalar::P256Scalar; +//! +//! let k = P256Scalar::from_limbs([42, 0, 0, 0]); +//! let point = comb_multiply_base_point(&k); +//! let (x, _y) = point.to_affine().expect("[k]G is not the identity for k in [1, n-1]"); +//! # let _ = x; +//! ``` +//! +//! `[u]G + [v]Q` for *public* scalars, the operation verification is built on. A separate scalar +//! type ([`p256_scalar::P256PublicScalar`]) selects the variable-time multiplier, so a secret +//! scalar cannot reach it by accident -- the type system enforces the split rather than a +//! reviewer: +//! +//! ``` +//! use bouncycastle_ec::p256::P256FieldElement; +//! use bouncycastle_ec::p256_domain::{G_X_LIMBS, G_Y_LIMBS}; +//! use bouncycastle_ec::p256_point::P256JacobianPoint; +//! use bouncycastle_ec::p256_scalar::P256PublicScalar; +//! use bouncycastle_ec::p256_wnaf::shamir_multiply; +//! +//! let g = P256JacobianPoint::from_affine( +//! P256FieldElement::from_limbs(G_X_LIMBS), +//! P256FieldElement::from_limbs(G_Y_LIMBS), +//! ); +//! let u = P256PublicScalar::from_limbs([3, 0, 0, 0]); +//! let v = P256PublicScalar::from_limbs([5, 0, 0, 0]); +//! +//! // [3]G + [5]([2]G) == [13]G +//! assert_eq!( +//! shamir_multiply(&u, &v, &g.double()).to_affine(), +//! comb_thirteen_g().to_affine(), +//! ); +//! # fn comb_thirteen_g() -> P256JacobianPoint { +//! # use bouncycastle_ec::p256_comb::comb_multiply_base_point; +//! # use bouncycastle_ec::p256_scalar::P256Scalar; +//! # comb_multiply_base_point(&P256Scalar::from_limbs([13, 0, 0, 0])) +//! # } +//! ``` +//! +//! SEC 1 encoding and decoding, with the SP 800-186 Appendix D.1.1.1 validation that decoding a +//! public key requires: +//! +//! ``` +//! use bouncycastle_ec::p256_sec1; +//! +//! let (x, y) = p256_sec1::decode(&hex_g()).expect("G is a valid uncompressed point"); +//! assert_eq!(p256_sec1::encode_uncompressed(&x, &y), hex_g()); +//! assert!(p256_sec1::decode(&[0x04; 65]).is_none(), "not on the curve"); +//! # fn hex_g() -> [u8; 65] { +//! # use bouncycastle_ec::p256::P256FieldElement; +//! # use bouncycastle_ec::p256_domain::{G_X_LIMBS, G_Y_LIMBS}; +//! # bouncycastle_ec::p256_sec1::encode_uncompressed( +//! # &P256FieldElement::from_limbs(G_X_LIMBS), +//! # &P256FieldElement::from_limbs(G_Y_LIMBS), +//! # ) +//! # } +//! ``` +//! +//! Every curve exposes the same shape, with its own module prefix: `p256`, `p384`, `p521`, +//! `p256k1`, `bp256r1`, `bp384r1`, `bp512r1`, `sm2`. +//! +//! # Status +//! +//! All eight curves -- P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, +//! brainpoolP512r1 and SM2 -- are complete through domain parameters, branch-free Jacobian point +//! arithmetic, the scalar field, constant-time fixed-base scalar multiplication for signing, +//! variable-time Shamir's-trick multiplication for verification, and SEC 1 point +//! encoding/decoding with public-key validation (SP 800-186 Appendix D.1.1.1; SEC 1 v2/SEC 2 v2 +//! for secp256k1; RFC 5639 for the brainpool curves). Signature schemes built on top live in +//! [`bouncycastle_ecdsa`](../bouncycastle_ecdsa/index.html) (the seven ECDSA curves) and +//! [`bouncycastle_sm2`](../bouncycastle_sm2/index.html). +//! +//! # Memory Usage +//! +//! Every type here is a fixed-size array of `u64` limbs with no indirection, so its in-memory size +//! is fully determined by the curve's width. There is no heap allocation anywhere in this crate. +//! +//! | Curve | `FieldElement` | `JacobianPoint` | `Scalar` | `ScalarField` | +//! |-----------------|----------------|-----------------|----------|---------------| +//! | P-256 | 32 | 96 | 32 | 32 | +//! | P-384 | 48 | 144 | 48 | 48 | +//! | P-521 | 72 | 216 | 72 | 72 | +//! | secp256k1 | 32 | 96 | 32 | 32 | +//! | brainpoolP256r1 | 32 | 96 | 32 | 32 | +//! | brainpoolP384r1 | 48 | 144 | 48 | 48 | +//! | brainpoolP512r1 | 64 | 192 | 64 | 64 | +//! | SM2 | 32 | 96 | 32 | 32 | +//! +//! All values are bytes, from `core::mem::size_of`. A `JacobianPoint` is three field elements +//! (`X`, `Y`, `Z`). P-521's 72 bytes hold a 521-bit value across 9 limbs rather than a tighter +//! bit-packing, which is why it exceeds the 66 bytes its SEC 1 encoding needs. +//! +//! The dominant *stack* cost is not these types but the fixed-base comb tables, which are `const` +//! data rather than stack: 64 entries of two field elements per curve (4 KiB for a 256-bit curve, +//! 9 KiB for P-521). [`p256_comb::comb_multiply_base_point`] scans all 64 entries under a mask on +//! every round, so that table is read in full regardless of the scalar -- see its docs. The +//! variable-time multiplier keeps a second, smaller `const` table per curve: the 32 odd multiples +//! of `G` its width-7 window over `G` indexes (2 KiB for a 256-bit curve, 4.5 KiB for P-521), +//! read by public digit and so with no scan. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! Every arithmetic primitive here is written to be branch-free and to avoid indexing by a +//! secret value, per this workspace's constant-time rules. See [`p256`]'s module docs for the +//! field's specific reduction algorithm and the reasoning behind it. +//! +//! Three deliberate exceptions, all marked in their own docs, all reachable only through a type +//! that says so: [`p256_wnaf::shamir_multiply`] and its siblings take +//! [`p256_scalar::P256PublicScalar`] and branch freely, because verification operates entirely on +//! public data; `P256PublicScalar::invert_vartime` and its siblings invert a public scalar +//! (verification's `s`) by the binary extended Euclidean algorithm in [`inverse_vartime`], for +//! the same reason; and `JacobianPoint::to_affine` branches on the point being the identity, so +//! it is for output boundaries on already-public points, not for secret intermediates. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +pub mod barrett; +pub mod bp256r1; +pub mod bp256r1_comb; +pub(crate) mod bp256r1_comb_table; +pub mod bp256r1_domain; +pub mod bp256r1_extra_bits; +pub mod bp256r1_mul; +pub mod bp256r1_point; +pub mod bp256r1_scalar; +pub mod bp256r1_sec1; +pub mod bp256r1_wnaf; +pub(crate) mod bp256r1_wnaf_table; +pub mod bp384r1; +pub mod bp384r1_comb; +pub(crate) mod bp384r1_comb_table; +pub mod bp384r1_domain; +pub mod bp384r1_extra_bits; +pub mod bp384r1_mul; +pub mod bp384r1_point; +pub mod bp384r1_scalar; +pub mod bp384r1_sec1; +pub mod bp384r1_wnaf; +pub(crate) mod bp384r1_wnaf_table; +pub mod bp512r1; +pub mod bp512r1_comb; +pub(crate) mod bp512r1_comb_table; +pub mod bp512r1_domain; +pub mod bp512r1_extra_bits; +pub mod bp512r1_mul; +pub mod bp512r1_point; +pub mod bp512r1_scalar; +pub mod bp512r1_sec1; +pub mod bp512r1_wnaf; +pub(crate) mod bp512r1_wnaf_table; +pub mod inverse_vartime; +pub mod montgomery; +pub mod nat; +pub mod p256; +pub mod p256_comb; +pub(crate) mod p256_comb_table; +pub mod p256_domain; +pub mod p256_extra_bits; +pub mod p256_mul; +pub mod p256_point; +pub mod p256_scalar; +pub mod p256_sec1; +pub mod p256_wnaf; +pub(crate) mod p256_wnaf_table; +pub mod p256k1; +pub mod p256k1_comb; +pub(crate) mod p256k1_comb_table; +pub mod p256k1_domain; +pub mod p256k1_extra_bits; +pub mod p256k1_mul; +pub mod p256k1_point; +pub mod p256k1_scalar; +pub mod p256k1_sec1; +pub mod p256k1_wnaf; +pub(crate) mod p256k1_wnaf_table; +pub mod p384; +pub mod p384_comb; +pub(crate) mod p384_comb_table; +pub mod p384_domain; +pub mod p384_extra_bits; +pub mod p384_mul; +pub mod p384_point; +pub mod p384_scalar; +pub mod p384_sec1; +pub mod p384_wnaf; +pub(crate) mod p384_wnaf_table; +pub mod p521; +pub mod p521_comb; +pub(crate) mod p521_comb_table; +pub mod p521_domain; +pub mod p521_extra_bits; +pub mod p521_mul; +pub mod p521_point; +pub mod p521_scalar; +pub mod p521_sec1; +pub mod p521_wnaf; +pub(crate) mod p521_wnaf_table; +pub mod sm2; +pub mod sm2_comb; +pub(crate) mod sm2_comb_table; +pub mod sm2_domain; +pub mod sm2_point; +pub mod sm2_scalar; +pub mod sm2_sec1; +pub mod sm2_wnaf; +pub(crate) mod sm2_wnaf_table; diff --git a/crypto/ec/src/montgomery.rs b/crypto/ec/src/montgomery.rs new file mode 100644 index 000000000..526157320 --- /dev/null +++ b/crypto/ec/src/montgomery.rs @@ -0,0 +1,210 @@ +//! Generic branch-free Montgomery multiplication primitives (widening multiply and REDC), +//! shared by curves whose modulus has no special (Solinas) structure to exploit for reduction -- +//! currently the brainpool curves (RFC 5639), whose primes are "random" with no algebraic +//! shortcut, unlike every NIST curve or secp256k1 in this crate. +//! +//! Every other curve here hand-writes its own `widening_mul`/`redc` (see e.g. +//! [`crate::p256_scalar`]'s docs on why that isn't expressed generically) because each one's +//! reduction is tuned to that specific modulus. That tuning is exactly what a "random" prime +//! doesn't offer, so there is nothing curve-specific left to hand-write per brainpool width -- +//! this module is genuinely shared, parameterized over the limb count `L` via a const generic +//! (matching [`crate::nat`]'s own `add`/`sub`), with the modulus and Montgomery constants passed +//! in at each call rather than baked in as per-curve compile-time constants. +//! +//! # Why a second const generic `L2` +//! +//! A widening multiply of two `L`-limb operands produces `2L` limbs, and Rust's stable const +//! generics cannot express `[u64; 2 * L]` as a type (that needs the unstable +//! `generic_const_exprs`, which this workspace does not enable; `crypto/mldsa/src/lib.rs` has a +//! commented-out attempt at a different const-generic feature for the same underlying reason). +//! So the width is passed as a second parameter `L2`, with a debug assertion that it really is +//! `2 * L`; every caller in this crate instantiates an `` pair. +//! +//! The obvious alternative -- and what this module did first -- is to dodge `L2` by returning the +//! product as two separate `L`-sized halves, `(low, high)`, and indexing logical limb `idx` as +//! `if idx < L { low[idx] } else { high[idx - L] }`. That is tidier to type and measurably +//! expensive: splitting the accumulator across two arrays cost **2.2x** per field multiplication +//! for brainpoolP256r1 (54.7ns against 24.6ns for the flat form), because the optimizer stops +//! keeping the accumulator in registers once it lives in two places. Hoisting the `idx < L` test +//! out of the inner loops, and forcing the whole thing to inline, were both tried first and +//! measured: the branch hoist changed nothing at all and `#[inline(always)]` bought about 8%. +//! Neither addressed the actual cost, which is the split itself. +//! +//! # REDC's bound +//! +//! [`redc`] implements the textbook "Separated Operand Scanning" (SOS) method (Koç, Acar & +//! Kaliski, *Analyzing and Comparing Montgomery Multiplication Algorithms*, 1996, Algorithm 2), +//! generalized to arbitrary `L`. Given `T = low + high*R < n*R` (`R = 2^(64L)`, the standard +//! Montgomery precondition, `n < R` the modulus), each of the `L` reduction rounds adds `m_i * n` +//! (`L + 1` limbs: a single-limb `m_i` times the `L`-limb modulus) at limb offset `i`; the last +//! round's offset is `L - 1`, so the addition can reach at most limb `(L - 1) + (L + 1) = 2L`, +//! meaning the running accumulator never needs more than `2L + 1` limbs -- provable directly from +//! this structure, not tuned per modulus, which is what makes it safe to generalize over `L` at +//! all. [`redc`]'s own doc comment has the exact bound the result satisfies. Verified (not checked +//! in) against 20,000 random trials plus the `(p-1)*(p-1)` worst case for each of brainpoolP256r1, +//! brainpoolP384r1, and brainpoolP512r1's actual primes (`L = 4, 6, 8`) before this was ported +//! from the Python prototype used to check it. + +/// Schoolbook widening multiply of two `L`-limb operands into their `L2 = 2L`-limb product. +// `inline(always)` on both entry points: with the modulus arriving as a runtime `&[u64; L]` +// rather than a per-curve constant, inlining is what lets each call site's own `P_LIMBS`/ +// `N_LIMBS` reach the multiply. Worth a consistent ~2% and measured, not assumed -- the two +// functions are small numeric kernels, so the code-size cost is limited to the three widths +// this crate instantiates. +#[inline(always)] +pub fn widening_mul(a: &[u64; L], b: &[u64; L]) -> [u64; L2] { + debug_assert_eq!(L2, 2 * L, "widening_mul's L2 must be exactly 2 * L"); + let mut result = [0u64; L2]; + for i in 0..L { + let mut carry: u128 = 0; + for j in 0..L { + let idx = i + j; + let prod = (a[i] as u128) * (b[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + // Mutating `carry = s >> 64` here (e.g. to `<< 64`) is an accepted mutant, not a bug: this + // loop's carry is always `0` or `1` at each step (`s` sums two u64-range values, so `s < + // 2^65`), and a value produced by `<< 64` has all its bits at position 64 or above -- so it + // contributes nothing to `get(..) as u128 + carry`'s low 64 bits at the next iteration, the + // stored limb is written unchanged, and the "carry" that comes back out is again pure high + // bits, propagating forever without ever affecting a stored limb (which is all this + // function returns) -- identical reasoning to [`crate::p521`]'s own `widening_mul`, whose + // docs have the full argument. Verified (not merely argued) over 100,000+ pseudorandom + // trials plus `(p-1)*(p-1)` for each of brainpoolP256r1/384r1/512r1's actual primes: the + // mutated function's output matches plain `u128`/bigint multiplication every time. + let mut k = i + L; + while k < L2 { + let s = (result[k] as u128) + carry; + result[k] = s as u64; + carry = s >> 64; + k += 1; + } + } + result +} + +/// Schoolbook squaring of an `L`-limb value into its `L2 = 2L`-limb square. +/// +/// `a * a` is symmetric: the product `a_i * a_j` appears twice for every `i != j`. Forming each of +/// those once and doubling costs `L(L+1)/2` limb multiplications instead of [`widening_mul`]'s +/// `L^2` -- ten against sixteen at `L = 4`, twenty-one against thirty-six at `L = 6`. +/// +/// Three passes: the off-diagonal products `a_i * a_j` for `i < j`; a doubling of the whole +/// accumulator; then the diagonal squares `a_i * a_i` added in at limb `2i`. Writing `result[i + +/// L]` in the first pass is an assignment rather than an accumulation because row `i` only ever +/// reaches limbs `2i + 1 ..= i + L - 1`, and no earlier row reaches limb `i + L` either, so that +/// limb is still zero when the row's final carry lands on it. The doubling cannot overflow: the +/// off-diagonal sum is strictly less than `a^2 / 2`. +/// +/// Verified against Python's arbitrary-precision `**2` over 20,000 random values at each of `L = +/// 4, 6, 8, 9` plus the all-ones worst case, before being written here. +#[inline(always)] +pub fn widening_square(a: &[u64; L]) -> [u64; L2] { + debug_assert_eq!(L2, 2 * L, "widening_square's L2 must be exactly 2 * L"); + let mut result = [0u64; L2]; + + // Off-diagonal products, each formed once. + for i in 0..L { + let mut carry: u128 = 0; + for j in (i + 1)..L { + let idx = i + j; + let prod = (a[i] as u128) * (a[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + result[i + L] = carry as u64; + } + + // Every off-diagonal product appears twice in the square. + let mut carry = 0u64; + for limb in result.iter_mut() { + let next_carry = *limb >> 63; + // `*limb << 1` always leaves bit 0 clear and `carry` is only ever 0 or 1, so the two + // operands are disjoint: mutating this `|` to `^` is an accepted equivalent, not a bug. + *limb = (*limb << 1) | carry; + carry = next_carry; + } + debug_assert_eq!(carry, 0, "doubling the off-diagonal sum overflowed 2L limbs"); + + // Diagonal squares. + let mut carry: u128 = 0; + for i in 0..L { + let square = (a[i] as u128) * (a[i] as u128); + let low = (result[2 * i] as u128) + ((square as u64) as u128) + carry; + result[2 * i] = low as u64; + let high = (result[2 * i + 1] as u128) + (square >> 64) + (low >> 64); + result[2 * i + 1] = high as u64; + carry = high >> 64; + } + debug_assert_eq!(carry, 0, "adding the diagonal overflowed 2L limbs"); + + result +} + +/// Montgomery reduction (SOS method): given `low`/`high` (a `2L`-limb value `T = low + high*R < +/// n*R`, the product of two values each `< n`), the `L`-limb modulus `n`, and `n' = -n⁻¹ mod +/// 2^64`, returns `T * R⁻¹`, `< 2n` (proven, see the module docs) -- callers still need their own +/// final conditional subtraction of `n` to reach the canonical `< n` representative, exactly as +/// every hand-written per-curve `redc` in this crate already does. +#[inline(always)] +pub fn redc( + t: &[u64; L2], + modulus: &[u64; L], + n_prime: u64, +) -> ([u64; L], u64) { + debug_assert_eq!(L2, 2 * L, "redc's L2 must be exactly 2 * L"); + debug_assert_eq!(L21, 2 * L + 1, "redc's L21 must be exactly 2 * L + 1"); + // The accumulator is `2L + 1` limbs (see the module docs for the bound) and is held flat, in + // one array. Keeping the top limb in a separate `u64` instead -- which is what this did before + // `L21` existed -- reintroduces exactly the split the module docs describe: measured, it cost + // 1.7x per multiplication for brainpoolP256r1 (42.7ns against 24.5ns), for the same reason the + // `(low, high)` product split did. + let mut acc = [0u64; L21]; + acc[..L2].copy_from_slice(t); + + for i in 0..L { + let m = acc[i].wrapping_mul(n_prime); + + // `m * modulus`: a single limb times an L-limb value, so it fits in L limbs plus one + // carry-out limb (never a full extra L-limb half). + let mut mn = [0u64; L]; + let mut mn_carry: u128 = 0; + for j in 0..L { + let prod = (m as u128) * (modulus[j] as u128) + mn_carry; + mn[j] = prod as u64; + mn_carry = prod >> 64; + } + let mn_top = mn_carry as u64; + + let mut carry: u128 = 0; + for j in 0..L { + let idx = i + j; + let s = (acc[idx] as u128) + (mn[j] as u128) + carry; + acc[idx] = s as u64; + carry = s >> 64; + } + // position i + L: add mn_top plus the carry just accumulated. + let s = (acc[i + L] as u128) + (mn_top as u128) + carry; + acc[i + L] = s as u64; + carry = s >> 64; + + // Propagate the carry through every limb above the window, up to the accumulator's + // proven `L21`-limb bound. The trip count depends only on the public round index `i`, + // never on the operands: once the carry has propagated out, the remaining iterations + // add 0. A `while carry != 0` here would be a secret-dependent branch on the signing + // path. + for k in (i + L + 1)..L21 { + let s = (acc[k] as u128) + carry; + acc[k] = s as u64; + carry = s >> 64; + } + debug_assert_eq!(carry, 0, "carry must be fully propagated within the 2L+1-limb bound"); + } + + debug_assert_eq!(&acc[..L], &[0u64; L][..], "REDC's low limbs must be cleared after L rounds"); + debug_assert!(acc[L2] == 0 || acc[L2] == 1, "REDC's top limb must be a single bit"); + let mut high = [0u64; L]; + high.copy_from_slice(&acc[L..L2]); + (high, acc[L2]) +} diff --git a/crypto/ec/src/nat.rs b/crypto/ec/src/nat.rs new file mode 100644 index 000000000..02071cd46 --- /dev/null +++ b/crypto/ec/src/nat.rs @@ -0,0 +1,45 @@ +//! Fixed-width, branch-free arithmetic on little-endian `[u64; L]` limb arrays. +//! +//! `L` is chosen per curve by its bit width (`L = 4` for a 256-bit field, as used by P-256). Every +//! primitive here is total, branch-free, and returns a plain carry/borrow (0 or 1) rather than +//! branching on it, so that callers doing modular arithmetic on secret values can chain these +//! without leaking timing through the carry/borrow chain itself. Comparisons belong to the +//! curve-specific field module (built from these primitives plus [`bouncycastle_utils::ct`]), +//! not here. + +/// Adds two `L`-limb little-endian numbers. Returns the sum and the carry out of the top limb (0 +/// or 1); the true (unwrapped) sum is `result + carry * 2^(64*L)`. +pub fn add(a: &[u64; L], b: &[u64; L]) -> ([u64; L], u64) { + let mut result = [0u64; L]; + let mut carry: u64 = 0; + for i in 0..L { + let (s1, c1) = a[i].overflowing_add(b[i]); + let (s2, c2) = s1.overflowing_add(carry); + result[i] = s2; + carry = (c1 as u64) | (c2 as u64); + } + (result, carry) +} + +/// Subtracts `b` from `a` over `L` limbs. Returns the difference and the borrow out of the top +/// limb (0 or 1); the true (unwrapped) difference is `result - borrow * 2^(64*L)`. +pub fn sub(a: &[u64; L], b: &[u64; L]) -> ([u64; L], u64) { + let mut result = [0u64; L]; + let mut borrow: u64 = 0; + for i in 0..L { + let (d1, b1) = a[i].overflowing_sub(b[i]); + let (d2, b2) = d1.overflowing_sub(borrow); + result[i] = d2; + borrow = (b1 as u64) | (b2 as u64); + } + (result, borrow) +} + +/// TRUE iff every limb of `a` is zero. +pub fn is_zero(a: &[u64; L]) -> bouncycastle_utils::ct::Condition { + let mut acc = 0u64; + for i in 0..L { + acc |= a[i]; + } + bouncycastle_utils::ct::Condition::::is_zero(acc) +} diff --git a/crypto/ec/src/p256.rs b/crypto/ec/src/p256.rs new file mode 100644 index 000000000..a095e66ba --- /dev/null +++ b/crypto/ec/src/p256.rs @@ -0,0 +1,414 @@ +//! The P-256 base field, GF(p) for `p = 2^256 - 2^224 + 2^192 + 2^96 - 1`. +//! +//! Domain parameters are from NIST SP 800-186 (Feb 2023) §3.2.1.3, "Curve P-256": the prime `p` +//! quoted there in hex is +//! `0xffffffff 00000001 00000000 00000000 00000000 ffffffff ffffffff ffffffff` (256 bits, most +//! significant word first). [`P_LIMBS`] below is that same value in little-endian `u64` limbs. +//! +//! # Reduction algorithm +//! +//! `p` is a generalized Mersenne number, so a 512-bit product reduces without any multiplication +//! at all: SP 800-186 (Feb 2023) Appendix G.1.2, "Curve P-256", gives `B = (T + 2S1 + 2S2 + S3 + +//! S4 - D1 - D2 - D3 - D4) mod p`, where each of the nine terms is a 256-bit value assembled by +//! concatenating 32-bit words of the product itself. [`reduce`] transcribes that expression +//! directly, so a reviewer with G.1.2 open can match its nine terms to the document row by row. +//! +//! G.1's own preamble states both the precondition -- "given an integer A less than m^2" -- and +//! the shape of the leftover work: "the integer sum or difference can be evaluated and the result +//! reduced modulo m. The latter reduction can be accomplished by adding or subtracting a few +//! copies of m." [`reduce`] carries the exact bound on how many copies that is here, and why, +//! after biasing the accumulator by `5p` so it never goes negative, a single fold of the top limb +//! plus a single conditional subtraction always lands in `[0, p)`. +//! +//! The one identity that outlives the reduction is `2^256 = p + C` where `C = 2^224 - 2^192 - +//! 2^96 + 1` (i.e. `2^256 mod p = C`): [`P256FieldElement::add`] uses it to correct a carry out +//! of the top limb, and [`reduce`]'s final fold uses it on the accumulator's top limb. +//! +//! Every step is a fixed, public-length loop over fixed-size arrays with a single masked select, +//! so the whole routine is trivially constant-time in the operands. + +use crate::nat; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// `p`, little-endian `u64` limbs (SP 800-186 §3.2.1.3). +pub const P_LIMBS: [u64; 4] = + [0xffffffffffffffff, 0x00000000ffffffff, 0x0000000000000000, 0xffffffff00000001]; + +/// `2^256 mod p = 2^224 - 2^192 - 2^96 + 1`, little-endian `u64` limbs. This is the constant +/// [`reduce`]'s fold uses in place of `2^256`. +const C_LIMBS: [u64; 4] = + [0x0000000000000001, 0xffffffff00000000, 0xffffffffffffffff, 0x00000000fffffffe]; + +/// `p - 2`, little-endian `u64` limbs -- the public exponent [`P256FieldElement::invert`] raises +/// its base to, per Fermat's little theorem. +const P_MINUS_2_LIMBS: [u64; 4] = + [0xfffffffffffffffd, 0x00000000ffffffff, 0x0000000000000000, 0xffffffff00000001]; + +/// `5p`, little-endian `u64` limbs -- the bias [`reduce`] starts its accumulator at so that +/// subtracting SP 800-186 §G.1.2's four `D` terms can never take it below zero. See [`reduce`] +/// for why `5` is the right multiple. +const FIVE_P_LIMBS: [u64; 5] = [ + 0xfffffffffffffffb, 0x00000004ffffffff, 0x0000000000000000, 0xfffffffb00000005, + 0x0000000000000004, +]; + +/// An element of the P-256 base field GF(p), always held in canonical reduced form (`< p`). +#[derive(Clone, Copy, Debug)] +pub struct P256FieldElement([u64; 4]); + +impl P256FieldElement { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0]); + + /// The multiplicative identity. + pub const ONE: Self = Self([1, 0, 0, 0]); + + /// Builds a field element from little-endian `u64` limbs, reducing once if `limbs >= p`. Any + /// `u64` limb pattern is accepted: since `limbs < 2^256 < 2*p`, a single conditional + /// subtraction of `p` always suffices to bring it into `[0, p)`. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + let (diff, borrow) = nat::sub(&limbs, &P_LIMBS); + let mut result = [0u64; 4]; + // borrow == 1 means limbs < p already; otherwise take the (single) reduced difference. + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut result); + Self(result) + } + + /// Returns the canonical little-endian `u64` limbs, `< p`. + pub fn to_limbs(&self) -> [u64; 4] { + self.0 + } + + /// The limbs exactly as this type stores them: the canonical value itself, so this is + /// [`Self::to_limbs`] by another name. Paired with [`Self::from_internal_limbs`] so the + /// crate's point arithmetic can select between the coordinates of two existing points without + /// re-reducing each one. + pub(crate) fn internal_limbs(&self) -> [u64; 4] { + self.0 + } + + /// Wraps limbs already in this type's stored representation (see [`Self::internal_limbs`]), + /// skipping the reduction and conversion [`Self::from_limbs`] performs. Only for values that + /// came out of another element of this type, or a constant computed in that representation: + /// the masked selects in the point arithmetic, and the comb table's infinity sentinel. Not + /// exposed, since an out-of-range value would break every equality and reduction assumption + /// downstream; the range is checked in debug builds. + pub(crate) fn from_internal_limbs(limbs: [u64; 4]) -> Self { + debug_assert_eq!( + crate::nat::sub(&limbs, &P_LIMBS).1, + 1, + "from_internal_limbs given a value >= p" + ); + Self(limbs) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + nat::is_zero(&self.0) + } + + /// Overwrites the element with zero, for scrubbing a secret-derived value before it goes out + /// of scope. Defence in depth rather than a guarantee: copies the compiler kept in registers + /// or spilled itself are not reached. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self + other mod p`. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = nat::add(&self.0, &other.0); + // true_sum = sum + carry*2^256, and self,other < p so true_sum < 2p. + let (sum_plus_c, _) = nat::add(&sum, &C_LIMBS); // used when carry == 1: true_sum - p == sum + C + let (diff, borrow) = nat::sub(&sum, &P_LIMBS); // used when carry == 0: is sum >= p? + let mut when_no_carry = [0u64; 4]; + // borrow == 1 means sum < p already; otherwise take sum - p. + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_c, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod p`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = nat::sub(&self.0, &other.0); + let (corrected, _) = nat::add(&diff, &P_LIMBS); // used when borrow == 1: (self - other) + p + let mut result = [0u64; 4]; + // borrow == 1 means self < other, so the wrapped diff needs +p added back. + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod p`. + pub fn negate(&self) -> Self { + let (diff, _) = nat::sub(&P_LIMBS, &self.0); // p - self; never borrows since self < p + let mut result = [0u64; 4]; + // p - 0 == p, which is not canonical: 0 must negate to 0, not p. + ct::conditional_select(nat::is_zero(&self.0), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod p`. + pub fn mul(&self, other: &Self) -> Self { + Self(reduce(&widening_mul(&self.0, &other.0))) + } + + /// `self^2 mod p`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`widening_square`]. + /// + /// Worth using in the exponentiation loops ([`Self::invert`], + /// [`crate::p256_sec1`]'s square-and-multiply), which call it hundreds of times in a row with + /// nothing else competing for registers: inversion measured ~25% faster. It is deliberately + /// *not* used by [`crate::p256_point`], whose `double`/`generic_add` square by calling `mul` + /// with equal arguments. That was tried and measured: despite `square` being ~9% cheaper than + /// `mul` in isolation, routing the point arithmetic through it made the constant-time comb + /// multiplier consistently slower (42.0us -> 42.3us over three interleaved rounds), since + /// inlining a third wide-multiply routine into that loop costs more than the four saved + /// partial products return. Left as it is on purpose. + pub fn square(&self) -> Self { + Self(reduce(&widening_square(&self.0))) + } + + /// `self^-1 mod p`, or `0` if `self` is `0`. + /// + /// Computed as `self^(p-2) mod p` (Fermat's little theorem: `self^(p-1) = 1` for `self != 0`, + /// FIPS 186-5 Appendix B.1, whose reference algorithm this deliberately does not use -- its + /// own text sanctions the substitution: *"The algorithm given below is for reference purposes. + /// Other (constant time) algorithms that produce an equivalent result may be used."*), by a + /// fixed 4-bit-window exponentiation: a table of `self^0 .. self^15` (14 multiplications), + /// then for each nibble of `p - 2` from the top, four squarings and one multiplication by the + /// table entry the nibble selects, skipped when the nibble is zero. That is `bits - 1` + /// squarings either way, but at most `14 + bits/4` multiplications instead of one per set bit + /// -- roughly half as many for these exponents -- against the bit-at-a-time version this + /// replaces. + /// + /// Constant time in `self`: the exponent is a compile-time public constant, so which nibbles + /// are zero -- hence which table entries are read, and in what order -- is fixed before `self` + /// exists. Indexing the table by a nibble of that constant is not a secret-dependent memory + /// access, and the `if` on it is a branch on public data, exactly as the bit test it replaces + /// was. No step branches on or indexes by `self`. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..4).rev() { + let limb = P_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } +} + +impl PartialEq for P256FieldElement { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..4 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for P256FieldElement {} + +/// Schoolbook widening multiply of two 4-limb (256-bit) operands into an 8-limb (512-bit) +/// product. Not expressed generically over `Nat`: stable Rust has no way to bound an output +/// array's length to `2*L` for a generic `L`, so each curve's field module writes its own +/// fixed-width widening multiply (the "field arithmetic is per curve" rule the crate's design +/// notes describe for the reduction, applied here too). +/// +/// Carry propagation always walks every remaining output limb for a given row, whether or not the +/// carry is actually still nonzero: the loop bound depends only on the (public) row index `i`, not +/// on the (potentially secret) operand values, so this does not leak timing through an +/// early-exit carry chain. +fn widening_mul(a: &[u64; 4], b: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + for i in 0..4 { + let mut carry: u128 = 0; + for j in 0..4 { + let idx = i + j; + let prod = (a[i] as u128) * (b[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + let mut k = i + 4; + while k < 8 { + let s = (result[k] as u128) + carry; + result[k] = s as u64; + carry = s >> 64; + k += 1; + } + } + result +} + +/// The 32-bit word `A_i` of the 512-bit product, in SP 800-186 §G.1.2's numbering: that appendix +/// writes the product as `A = (A15 || A14 || ... || A0)` with each `A_i` a 32-bit integer, `A0` +/// least significant. +fn word(t: &[u64; 8], i: usize) -> u64 { + (t[i / 2] >> (32 * (i % 2))) & 0xffff_ffff +} + +/// Assembles one of §G.1.2's 256-bit terms from its eight 32-bit words. The document prints each +/// term most significant word first (`( A15 || A14 || ... )`); this takes them least significant +/// first, so each call site below reads its document row right to left. +fn term(w: [u64; 8]) -> [u64; 4] { + [w[0] | (w[1] << 32), w[2] | (w[3] << 32), w[4] | (w[5] << 32), w[6] | (w[7] << 32)] +} + +/// Zero-extends a 4-limb value to 5 limbs, for arithmetic against the 5-limb accumulator. +fn widen(v: &[u64; 4]) -> [u64; 5] { + [v[0], v[1], v[2], v[3], 0] +} + +/// `small * v`, one limb times a 4-limb value, as 5 limbs. Only ever called with a `small` that +/// is a bounded, operand-independent count (see [`reduce`]), never a secret. +fn mul_small(small: u64, v: &[u64; 4]) -> [u64; 5] { + let mut out = [0u64; 5]; + let mut carry: u128 = 0; + for i in 0..4 { + let prod = (small as u128) * (v[i] as u128) + carry; + out[i] = prod as u64; + carry = prod >> 64; + } + out[4] = carry as u64; + out +} + +/// Schoolbook squaring of a 4-limb value into its 8-limb square. +/// +/// `a * a` is symmetric: the product `a_i * a_j` appears twice for every `i != j`. Forming each of +/// those once and doubling the result costs `L(L+1)/2` limb multiplications -- ten at this width +/// against the sixteen [`widening_mul`] would form for the same value. +/// +/// Three passes: the off-diagonal products `a_i * a_j` for `i < j`; a doubling of the whole +/// accumulator; then the diagonal squares `a_i * a_i` added in at limb `2i`. Writing `result[i + +/// L]` in the first pass is an assignment rather than an accumulation because row `i` only ever +/// reaches limbs `2i + 1 ..= i + L - 1`, and no earlier row reaches limb `i + L` either, so that +/// limb is still zero when the row's final carry lands on it. The doubling cannot overflow: the +/// off-diagonal sum is strictly less than `a^2 / 2`. +/// +/// Verified against Python's arbitrary-precision `**2` over 20,000 random 4-limb values plus the +/// all-ones worst case before being written here. +fn widening_square(a: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + + // Off-diagonal products, each formed once. + for i in 0..4 { + let mut carry: u128 = 0; + for j in (i + 1)..4 { + let idx = i + j; + let prod = (a[i] as u128) * (a[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + result[i + 4] = carry as u64; + } + + // Every off-diagonal product appears twice in the square. + let mut carry = 0u64; + for limb in result.iter_mut() { + let next_carry = *limb >> 63; + // `*limb << 1` always leaves bit 0 clear and `carry` is only ever 0 or 1, so the two + // operands are disjoint: mutating this `|` to `^` is an accepted equivalent, not a bug. + *limb = (*limb << 1) | carry; + carry = next_carry; + } + debug_assert_eq!(carry, 0, "doubling the off-diagonal sum overflowed 8 limbs"); + + // Diagonal squares. + let mut carry: u128 = 0; + for i in 0..4 { + let square = (a[i] as u128) * (a[i] as u128); + let low = (result[2 * i] as u128) + ((square as u64) as u128) + carry; + result[2 * i] = low as u64; + let high = (result[2 * i + 1] as u128) + (square >> 64) + (low >> 64); + result[2 * i + 1] = high as u64; + carry = high >> 64; + } + debug_assert_eq!(carry, 0, "adding the diagonal overflowed 8 limbs"); + + result +} + +/// Reduces an 8-limb (512-bit) value modulo `p`, per SP 800-186 §G.1.2 (see the module docs). +/// +/// **Precondition: `t < p^2`**, which is G.1's own stated precondition ("given an integer A less +/// than m^2") and holds for every call site, since `mul` is the only one and it passes the product +/// of two canonical (`< p`) field elements. The bounds below all rest on it; a raw 512-bit value +/// near `2^512` is *not* reduced correctly by this function. +/// +/// Bounds. Each of the nine terms is a 256-bit value, so `T + 2*S1 + 2*S2 + S3 + S4 < 6*2^256` +/// and `D1 + D2 + D3 + D4 < 4*2^256`. Starting the accumulator at `5p` therefore keeps it +/// non-negative throughout (`5p > 4*2^256`, since `5p - 4*2^256 = 2^256 - 5*2^224 + ... > 0`) and +/// bounded by `5p + 6*2^256 < 11*2^256`, so five limbs are always enough and the top limb `u_hi` +/// is at most 10. Folding that top limb back in via `2^256 ≡ C` adds `u_hi*C < 11*2^224 < 2^228`, +/// leaving `V < 2^256 + 2^228`; since `p > 2^256 - 2^224`, `V - p < 2^228 + 2^224 < p`, so exactly +/// one conditional subtraction finishes the job. +fn reduce(t: &[u64; 8]) -> [u64; 4] { + let a = |i: usize| word(t, i); + + // SP 800-186 §G.1.2's nine terms, each row read right to left from the document. + let t_term = term([a(0), a(1), a(2), a(3), a(4), a(5), a(6), a(7)]); + let s1 = term([0, 0, 0, a(11), a(12), a(13), a(14), a(15)]); + let s2 = term([0, 0, 0, a(12), a(13), a(14), a(15), 0]); + let s3 = term([a(8), a(9), a(10), 0, 0, 0, a(14), a(15)]); + let s4 = term([a(9), a(10), a(11), a(13), a(14), a(15), a(13), a(8)]); + let d1 = term([a(11), a(12), a(13), 0, 0, 0, a(8), a(10)]); + let d2 = term([a(12), a(13), a(14), a(15), 0, 0, a(9), a(11)]); + let d3 = term([a(13), a(14), a(15), a(8), a(9), a(10), 0, a(12)]); + let d4 = term([a(14), a(15), 0, a(9), a(10), a(11), 0, a(13)]); + + // B + 5p = 5p + T + 2*S1 + 2*S2 + S3 + S4 - D1 - D2 - D3 - D4, over five limbs. `S1` and `S2` + // appear twice rather than being doubled, so every step is the same 5-limb add. + let mut acc = FIVE_P_LIMBS; + for addend in [&t_term, &s1, &s1, &s2, &s2, &s3, &s4] { + let (sum, carry) = nat::add(&acc, &widen(addend)); + debug_assert_eq!(carry, 0, "P-256 reduction accumulator overflowed 5 limbs"); + acc = sum; + } + for subtrahend in [&d1, &d2, &d3, &d4] { + let (diff, borrow) = nat::sub(&acc, &widen(subtrahend)); + debug_assert_eq!( + borrow, 0, + "P-256 reduction accumulator went negative despite the 5p bias" + ); + acc = diff; + } + + // Fold the accumulator's top limb back in: `2^256 ≡ C (mod p)`. + debug_assert!(acc[4] <= 10, "P-256 reduction top limb exceeded its proven bound"); + let low: [u64; 4] = [acc[0], acc[1], acc[2], acc[3]]; + let (v, carry) = nat::add(&mul_small(acc[4], &C_LIMBS), &widen(&low)); + debug_assert_eq!(carry, 0, "P-256 reduction fold overflowed 5 limbs"); + + let (diff, borrow) = nat::sub(&v, &widen(&P_LIMBS)); + let mut selected = [0u64; 5]; + ct::conditional_select(Condition::::from_lsb(borrow), &v, &diff, &mut selected); + debug_assert_eq!(selected[4], 0, "P-256 reduction left a value >= 2^256"); + [selected[0], selected[1], selected[2], selected[3]] +} diff --git a/crypto/ec/src/p256_comb.rs b/crypto/ec/src/p256_comb.rs new file mode 100644 index 000000000..5e2a656a0 --- /dev/null +++ b/crypto/ec/src/p256_comb.rs @@ -0,0 +1,108 @@ +//! Fixed-base comb scalar multiplication: `[k]G` for P-256's base point `G`, with `k` secret. +//! +//! This is the standard fixed-point comb multiplication algorithm, width 6 (the standard choice +//! for fields over 250 bits), without the "offset" that a generic-curve implementation carries. +//! +//! # No offset +//! +//! The comb *does* reach the identity: the accumulator is [`P256JacobianPoint::INFINITY`] going +//! into round 0, and comb digit 0 selects the identity from the table in any later round. A +//! generic-curve comb (bc-java's `FixedPointCombMultiplier`, for one) biases every table entry by +//! `G` so that no entry is the identity and the accumulator is never the identity after round 0, +//! then cancels the accumulated bias with a final offset add. It needs that because its point-add +//! is generic over curves and branches on `isInfinity`; the bias keeps that branch off any +//! secret-dependent path. It does nothing for the same-point and opposite-point cases, which such +//! an add also branches on -- those are left to be negligibly rare. +//! +//! Here the add is complete instead. `P256JacobianPoint::add_affine` computes the generic-add +//! and doubling candidates unconditionally on every call and masks in all four exceptional cases +//! (either operand the identity, same point, opposite point), with the table's identity entry +//! passed as a flag rather than as a `Z = 0` coordinate; see +//! [`crate::p256_point`]'s module docs. There is no branch left for an offset to keep the identity +//! away from, so an offset would cost a point addition and a biased table and buy nothing. The +//! accumulator starts at the identity and the algorithm is the plain comb. +//! +//! # The comb +//! +//! For width `w = 6` and the scalar's bit length `BITS = 256` (the P-256 order's bit length, SP +//! 800-186 §3.2.1.3), `D = ceil(BITS / w) = 43` and `FULL_COMB = D * w = 258` (two bits more than +//! `BITS`, zero-padded). [`COMB_TABLE_X`]/[`COMB_TABLE_Y`] (in [`crate::p256_comb_table`]) hold, +//! at index `i`, the point `sum_{bit b set in i} 2^(b*D) * G` -- i.e. index `i`'s table entry +//! encodes a width-6 "comb digit" as a subset sum of the doublings `2^(0*D)*G, 2^(1*D)*G, ..., +//! 2^(5*D)*G`. +//! +//! [`comb_multiply_base_point`] processes `D` rounds; round `i` reads the comb digit made of bits +//! `top-i, top-i-D, top-i-2D, ..., top-i-5D` of `k` (`top = FULL_COMB - 1`), doubles the running +//! accumulator, and adds the table entry for that digit. Every one of the `D*w = FULL_COMB` bit +//! positions is read by exactly one (round, digit-position) pair (verified, not checked in, by +//! confirming the six per-round ranges tile `0..FULL_COMB` with no gaps or overlaps), and every +//! step -- bit extraction, the table lookup, the add -- is branch-free in `k`'s value: the *bit +//! positions* read are fixed by the (public) loop structure, and [`table_lookup`] scans every +//! entry under a mask rather than indexing by the secret digit. +//! +//! Verified (not checked in) against 200 random `k` plus edge cases (`k = 1, 2, 3, n-1`), each +//! cross-checked against the SP 800-186 Appendix A.1.1 affine group law via repeated doubling and +//! conditional addition (a world apart from the comb, so this isn't just checking the algorithm +//! against itself). + +use crate::p256::P256FieldElement; +use crate::p256_comb_table::{COMB_TABLE_X, COMB_TABLE_Y}; +use crate::p256_point::P256JacobianPoint; +use crate::p256_scalar::P256Scalar; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 6; +const BITS: usize = 256; +const D: usize = BITS.div_ceil(WIDTH); +const FULL_COMB: usize = D * WIDTH; +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]G`, branch-free in `k`. +pub fn comb_multiply_base_point(k: &P256Scalar) -> P256JacobianPoint { + let limbs = k.limbs(); + let mut r = P256JacobianPoint::INFINITY; + let top = (FULL_COMB - 1) as isize; + for i in 0..D { + let mut secret_index: usize = 0; + let mut j = top - i as isize; + while j >= 0 { + let bit = if (j as usize) < BITS { + (limbs[(j as usize) / 64] >> ((j as usize) % 64)) & 1 + } else { + 0 + }; + secret_index = (secret_index << 1) | (bit as usize); + j -= D as isize; + } + r = r.double(); + let (x, y, is_infinity) = table_lookup(secret_index); + r = r.add_affine(&x, &y, is_infinity); + } + r +} + +/// Looks up comb table entry `secret_index` as affine coordinates plus an is-the-identity flag, +/// scanning every one of the [`TABLE_SIZE`] entries +/// under a mask so the index never steers which memory is read: a cache-safe lookup table. +fn table_lookup(secret_index: usize) -> (P256FieldElement, P256FieldElement, Condition) { + let mut x_limbs = [0u64; 4]; + let mut y_limbs = [0u64; 4]; + for i in 0..TABLE_SIZE { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + let mut next_x = [0u64; 4]; + ct::conditional_select(is_this_entry, &COMB_TABLE_X[i], &x_limbs, &mut next_x); + x_limbs = next_x; + let mut next_y = [0u64; 4]; + ct::conditional_select(is_this_entry, &COMB_TABLE_Y[i], &y_limbs, &mut next_y); + y_limbs = next_y; + } + // Entry 0 is the point at infinity; every other entry is affine (Z == 1). The accumulator's + // `add_affine` takes the identity as a flag rather than as a `Z == 0` coordinate. + let is_infinity = Condition::::is_equal(secret_index as u64, 0); + ( + P256FieldElement::from_internal_limbs(x_limbs), + P256FieldElement::from_internal_limbs(y_limbs), + is_infinity, + ) +} diff --git a/crypto/ec/src/p256_comb_table.rs b/crypto/ec/src/p256_comb_table.rs new file mode 100644 index 000000000..607b42b7b --- /dev/null +++ b/crypto/ec/src/p256_comb_table.rs @@ -0,0 +1,212 @@ +//! The checked-in fixed-base comb table for `[k]G` (P-256's base point `G`), width 6, the standard +//! choice for fields over 250 bits. See [`crate::p256_comb`] for the multiplier that uses this +//! table and the algorithm it implements. +//! +//! `COMB_TABLE_X[i]`/`COMB_TABLE_Y[i]` are the affine `(x, y)` coordinates of the `i`-th table +//! entry, as little-endian `u64` limbs; entry `0` is the point at infinity (`x = y = 0`, which is +//! not a valid affine coordinate pair for any point on the curve, so it's an unambiguous sentinel +//! here). +//! +//! Regenerated and compared against these checked-in values by this module's own `tests` +//! submodule below, using the same construction `examples/generate_p256_comb_table.rs` does -- so +//! a hand-edit or transcription error in this file cannot survive `cargo test`. A unit test rather +//! than an integration test because the table is deliberately `pub(crate)`, not exposed outside +//! the crate (it's an implementation detail of [`crate::p256_comb`]'s multiplier, not something a +//! caller should ever need directly), so an external integration test cannot reach it -- +//! QUALITY_AND_STYLE's stated exception for high-risk code unreachable through the public API. + +pub(crate) const COMB_TABLE_X: [[u64; 4]; 64] = [ + [0, 0, 0, 0], + [0xf4a13945d898c296, 0x77037d812deb33a0, 0xf8bce6e563a440f2, 0x6b17d1f2e12c4247], + [0xcd013f88b049e7cd, 0xe8f9257ae57fdc00, 0x3be71969fc3a9301, 0x987f256d58cff937], + [0x59db167c5a1c3fb1, 0x98b3ce2abf318eb2, 0x2df1c41ed2bc2fa6, 0xefcc2c436ed1b2af], + [0xfdc73e83bf780c2c, 0xffdc67942d666817, 0xc14b66dd02436893, 0x6eec95670d54650c], + [0xaec902647318188e, 0x410bec28ca167099, 0xbf664d2f099c202b, 0x13ccca3455fa625c], + [0xaca2fa084862c5db, 0xddffc222a1717f8a, 0xab839a14e4e09fd2, 0xf86a9078980330f5], + [0xd3b22809cbdb1c78, 0x5591c8eb30f6cda4, 0xb6e28740bfe80f8b, 0x0f74342a40e7e7e7], + [0xeb0421211a6b665e, 0x802f779ea7f6803a, 0x47501f2a3c0804c3, 0xa263919b4945a1d4], + [0x2b52c47d8b21aa51, 0x0f5036295a7e870d, 0xbaa9281488b45127, 0x27d6451ec402e050], + [0xd8142dff5788c0f6, 0x89bf5229247fde25, 0x5c971ddb14e2280f, 0x785b7e9109904e3f], + [0x73b7c5502195a979, 0x2d7ed474b8dd5813, 0xc0b9ecd2e104e9ac, 0xdc90d975a2bd0ed8], + [0xe4b541b6cf9a3ca9, 0x1c65058708b49b2f, 0xb95f91b3f552641e, 0xbddc23ac5c301277], + [0xcb4dc35b1703406d, 0x4fd3afc975dac54c, 0x112321eb29f02878, 0xafb18d2fad6b225f], + [0xff0e1f34e834a3c4, 0x0d59b6ae1c4ab236, 0x10eb194a015a211b, 0xed6e13e03892ddc5], + [0x2dc61e1b336aaf40, 0x897e87bd4251f5b7, 0x2fb320236511b370, 0x460fa9cf2341f499], + [0x3cd5f4e4a9aa52df, 0x18c452b1b42a627f, 0x6dbc4189d991ece6, 0x45a511c97f608bf7], + [0xc6e4b6d0016476ea, 0x71b9a7e5d4ec2510, 0x1975b71ecbe490d2, 0xdf6b472fb52acd25], + [0x86a38d54c219c20b, 0xafcdd2cab50a4733, 0xf4cf879772096638, 0xd949caa224ce0e94], + [0x41e9420620b4d697, 0xa10fd0d929fa0df9, 0xf11eb0a776022c38, 0xffcb7ddca5621c63], + [0x48d63748b56bc451, 0x0544de81a939440a, 0xda24eb0b664ec19c, 0x4fb6e56241f42bf6], + [0x546a08e7f119b8cc, 0x03b7d5238afc696a, 0x0a896132459f70b4, 0x57a46257a86a9116], + [0x6dd25e26e83cfa35, 0x61e44da01ff3bddc, 0xb7b67b02121733fa, 0x7c48f60dfcd798ca], + [0xf41e030756c8815e, 0xbaf647e37d37a2f1, 0x7791eb36fefafbf5, 0x158262fb35b7f606], + [0x9d7f749e7fd58ae5, 0xc78ba26337ea57a2, 0xb5c051274f5ab5b7, 0x6fd3f54d5f2d643b], + [0x5b2911dd4d6a3def, 0x4bedd07cb96008f1, 0xee748a6fe36e7d64, 0xbfc499344bbf5cf4], + [0x75ea69cbe76ccbc0, 0xc9736051a762deb7, 0xa720d4c6af2bff4c, 0x8e4c7b10be6d6dba], + [0x9cdda82186eb7815, 0x8c003612ce413265, 0x8bce1fab91b577f5, 0x0f3f29ff488f730c], + [0x6c10cdd296bf8ea5, 0xe28c488ae8cd868f, 0xba9226c346442d00, 0x9125caedfa1f864b], + [0x0d17ff3970514a21, 0xd2a7b5badadd80ee, 0x941e33c38126c8c4, 0xb9e156d01d57c1de], + [0xe0cb10291b20d599, 0x7b1ed83d10a5fba0, 0x7d5fb32b04007713, 0x93bab59079c82639], + [0xf197735bc05131cd, 0x0565076822beb567, 0xdbf2b189f7f55b1f, 0xaa144c82132c2614], + [0xf0f679f10b79847d, 0x3719a8b66bb19be6, 0x2ddb6c3ddc7f43d5, 0x2800043ada0982e2], + [0x12416a5c5f3f5b80, 0x58e903dbda522422, 0x18cc80f14291867e, 0xb2035cf87a152c2b], + [0x1caab0ba6aa2b49d, 0x6a75a7686f7fc502, 0x6a5ea5a857ea120f, 0x998cd5f9db6bdf96], + [0x40429d1bfea77b0c, 0x4651a4dc595e9a31, 0x8900aab1e712693a, 0x90ea776784bf612d], + [0x7a2f10b25ac0b3db, 0xe6deffa0f0b98928, 0xb4b2939be6b0b01a, 0xa03e1d520a3f2ca8], + [0x6890b26ce62da069, 0xa57023197c586265, 0xe64e19bf865672ab, 0xa66503f5a07d9893], + [0x74814e1c89806e19, 0x9135fc8df9ec85de, 0x0ee660a609afd25b, 0x943de3b76740a284], + [0xa3c9d614c4e48158, 0xb26b4a98ae8fc508, 0x44ef8be038b68e18, 0xbe9cf596db271fcd], + [0x0abfa3ee2d00715b, 0xf3f65dc1c8297b47, 0x4199b65900669e85, 0x7588df7f23c09567], + [0xc0b161fbf77cf152, 0x243c4fed8ce30043, 0xb1b4a2d0050e20df, 0x5a61a286c34999ae], + [0xa6bcc84d016f613c, 0xae5ce038c2ec4e56, 0xad80f035f8be76b4, 0x00456c5c84642dd4], + [0xc79e3178966d28dd, 0x67ba868689f8a2c1, 0xaf1f9c6d4acf8d42, 0x2d2b4273e0847f7d], + [0x2a675b54620c767b, 0xf1235f085ae6598e, 0x3cf6a1cd48a35e9b, 0xf11a113ed8a1b5f8], + [0x1c12b5cbd84a37de, 0x56d66db4c7b1ea1a, 0x852be4202ce31e9a, 0x17be9c2de40faf48], + [0x422618c90ccf3981, 0x7f5f96108dab3936, 0xca4ab7508e0a6a28, 0x8266e2fed5bab133], + [0x54b44d3300b16f35, 0x59988ef3002d5707, 0x256fe1ebd0494f94, 0xaef841697f710de4], + [0xb246602701feea35, 0xea17f580317c61f1, 0x8d71eaba786aaceb, 0x7de7454a1cc47dab], + [0x6033d113edab9cb9, 0x1df87ba3e69d45ee, 0x93436236e4d65a03, 0x5893f6f93f98a508], + [0x1c1dd91a68c28f39, 0xfa494334f35669ca, 0x77b40abd51abb743, 0xee7400bae7873a25], + [0x3276c5a4c52427d8, 0x66958243f5a34b64, 0x04166798f36e0d92, 0x43e33927c6e9e63f], + [0xe17f627b98a174fc, 0x5ebce1ff4dfa285e, 0xc95fe23d54c5f925, 0x5ea59a093188ba78], + [0xce46a1650758035b, 0xb33df1ade070a0c9, 0xbf01fb38686934c9, 0x1cba6257f0f16ed0], + [0xbf93cda851a03105, 0xb14f4a607be433ed, 0x0aa4c4c3fa1c97a1, 0xfe1a6375bced726e], + [0xfe702b4b27ade63f, 0x5df11a33a105673a, 0x0d33cb80a362b9ce, 0xa7bb42f5855bb209], + [0x252566b651ff89bb, 0x453c333edb973ddc, 0xfbcd5a09d83f2cc2, 0x187818ec3121dbd5], + [0x2096d6762da7eb49, 0x6e04768efb775e41, 0xc3349c3daf24f76c, 0xe6db6ccade0c90f6], + [0xeaf7623ce4976dd8, 0x92528b1ae29bd0b4, 0x78158ecd645cec2a, 0x3265ead8b11325e9], + [0x8e35bf16b3571976, 0xe2eb0c63346864e7, 0x2b7b57e07e9b6c7f, 0x3157cf6f70b35a98], + [0x3c3d8c9b6c38b3da, 0x80818302754433e3, 0xfe68ab07e29e542a, 0x81a25a61d12cbb2c], + [0xf3f4e3fefcf866b9, 0x152a0807e18b0ad5, 0x2ec4c7061b9b2e7b, 0x41d7e92bdadd006f], + [0x651ebb868916a00d, 0xba4d2da9001e908d, 0x5f2b68e61684fcb0, 0xc3ff8d7510ac6edf], + [0xb9e437f4afcc2bef, 0x4f1fb2d63ada2b53, 0xe6c0e12dbb580c9a, 0x2518373433c7546d], +]; + +pub(crate) const COMB_TABLE_Y: [[u64; 4]; 64] = [ + [0, 0, 0, 0], + [0xcbb6406837bf51f5, 0x2bce33576b315ece, 0x8ee7eb4a7c0f9e16, 0x4fe342e2fe1a7f9b], + [0xb7254bbc6efa35d6, 0x47b4605207aaffdb, 0xe860ebd60007e39e, 0x8e92695694ec505c], + [0x17fe07f197b25513, 0x468245333734a589, 0xa5384a77ed34f543, 0xf3684f9c8d9f3863], + [0x089ec1a1edbfcd32, 0x79ab66153a07ff89, 0xfc281de065ea0105, 0x14bb5350997732c2], + [0xaa84c23105421c0c, 0x6b6475216cdb0d71, 0xe90446b1fb216a5e, 0x4b5ba5a5af46893d], + [0x6890f24cc1dd7dcc, 0xf75dccfaea6efd98, 0xba2612b8ff9a093b, 0x20347d0c2568653c], + [0xd2968e87351c51f2, 0x65c5c581f5e17b5e, 0x6f58f02a9d994e2e, 0x531c0b00f5c1ec07], + [0x9ee4040030bcdcfb, 0xac3f83df4c00efe2, 0x2e9d3c9de60d60c5, 0x873200bd2aed20fc], + [0x5c96ec145567432d, 0xcdeb98290f4150c7, 0x5d91740ccdeef566, 0x2a58fa5e1be9e583], + [0x445e45192e7e6f0b, 0x8789440e4ce293dd, 0x96b84f57c797be30, 0x6b44059dfa3ea32d], + [0x9fb552034dd6eb2e, 0x50d554bbc01dfde8, 0x4cfd3277f0977a30, 0xc87ce232815374c4], + [0x519d070004daba43, 0xc003dcc38450cfa2, 0x73a1c8f54e48efde, 0x7d0ca9425b04f761], + [0xddf58273f1776a67, 0x96889755f6b96c2f, 0x31a8d66322208ffb, 0x5ed81c10fcca4877], + [0xac88df04fb3f678d, 0x6f0fbf44544026a9, 0xcde8cd7a619cecba, 0x02f322e580d9a8cc], + [0x03e63b79cbaf01a7, 0x937e123f44157434, 0x9d59226e809e4a1a, 0x18d6f63a41775e62], + [0x7b52bd12125ec16c, 0x5a919b27d22955ce, 0x3fe3337fcb625ad2, 0x73be0ec773ea9b6d], + [0xf1738716784055eb, 0xccc7b0b3b87d399e, 0x3c9a13371bb51119, 0xb42639e1a88fd593], + [0x678664ae96f9ae13, 0x00ef5ba9c984de46, 0x622abc7f8d549567, 0x673ed50057db924d], + [0x24e37b1b0927965a, 0x8d9fc102bd2c199e, 0x862de75e907f3f85, 0xd39851295a9c778e], + [0x21b2c80e66bb5d6b, 0xa4123924d25bd41b, 0x6f95f5f2bce2d418, 0xa92327764d6d91d8], + [0xfaa56fefbb314c65, 0xf4e61f4074795c6d, 0x1a3c5652437850d6, 0x7c4b127d6621ec11], + [0x244d234a090f5154, 0x93b7f2fb8cae33bb, 0x158bf2f6426d1516, 0xa8a947a8a801e86e], + [0xf6c3225532dce9e5, 0x6c7cd4ce361b4780, 0xe5be5e703f85288f, 0x4c281aa3c98e624a], + [0x3428e3112116b8ce, 0xc52d1d2471b28987, 0x87f70be98299421f, 0x0a5fd09864f49798], + [0x55c6f62d8e74750f, 0x22639f8748919902, 0xfa01aa94958a248f, 0x2743ae8aed51aa40], + [0xaf5c0efe2f128433, 0x834cbf1fa1fe85ec, 0xd321c5a62685f018, 0xb5b09cf6717a5340], + [0xebb08063e6960d55, 0x1a9699e2aecbf467, 0x6b1564a44ce5761b, 0x08f00ea581382996], + [0xf33bd66e2e21b4af, 0x12dc553768dbe58c, 0xd9b85123e5353044, 0xf4925bde07bc6b60], + [0x220d500dea8105ad, 0x6a2aa4620202f3ae, 0x450056ab3dc96356, 0x506ab6aa452142c3], + [0x977fa5a649b97d9d, 0xa35923333551254a, 0x8f277388a9f7a3eb, 0x36aba935e3026e2c], + [0xf41cbe14b3822251, 0xb1ce72b2ffd0afbe, 0x01a14d18844743fa, 0xc1d89fe3923739b8], + [0xfe5b0083908d9eda, 0xa87058dbb8513ae9, 0xb6c0796584a4dc3b, 0x0f99174667e82909], + [0x7112569195c80ede, 0xbfe02568af97c5b0, 0x603e1dc58a14e493, 0xf12f359c749680de], + [0xd2d7ba4c467184a9, 0xbe178e5425c03723, 0x6bfc1707bc389ef3, 0x3256a8a07b7d9fb3], + [0xbdd104250d02f2b6, 0xf5583bccfb4d594f, 0x757544625ba7b6a1, 0xd1a321d3101e86f4], + [0xfc7795312cbead24, 0xe8362908d30fa3f9, 0x6f29d6f4f23b00bb, 0xea1ad22febb82e0a], + [0xe4deb7c021fe4743, 0x3bae847d7d7100be, 0x1769fca7e17b1d29, 0xadba60ec320afc60], + [0xdba0327f622227d9, 0xa524c6d6d4c486e8, 0x217fb7797134581a, 0xafa3b65fe4254a7e], + [0x737b653e8e6f95ad, 0x73dbe6ff9b9e4d0a, 0x4b772a8ca4139f59, 0xa1f335e566c67e8a], + [0xabdf62fa868d3227, 0xa0844d348099a8fc, 0x3361b9c03babbc72, 0xbb0357a46d5bf03b], + [0x8c7baf6870214eb7, 0x975bca7df2c261fe, 0x03c6df311ed91ae8, 0xe8cfaaada1380d38], + [0x0ef7079fde3648c8, 0x7bf0b3ab68d0a170, 0xa85c96b856c684e3, 0xfd39b0f291d65c88], + [0x1d9e1a9069130cec, 0x95cb10fd9383e7b5, 0x73438a2644cc71ae, 0x37eaeb101ee4ea49], + [0xa401985d1742a887, 0x3f83bd07b6a73d9b, 0x3c7307a082736067, 0x64a1a66d1f12fbb6], + [0x735b3ccb38cc8797, 0x1f8d9d8034b1093e, 0xd8cc6e86e75b81c0, 0x6914bf943fdbe697], + [0xfaa7545bab5500f6, 0xa91edaeb5d994d86, 0x0a5b194b67fb462d, 0x089cfd68287178ce], + [0xca38fb1f8bd49604, 0xaec9daaebfa0b15c, 0x1551365e642cf6dd, 0x75b8b0fa160e8fff], + [0x10b69d62ff1b1266, 0xe22cc59bb9ab079c, 0x9a57e43f42b2d441, 0x22340fece8c85f85], + [0xb3832e15aad54fab, 0x3277ff0d6bc7365e, 0xe8301118200c4fb8, 0x26e471bcd4e9384d], + [0xf15d9bf5ed2309d9, 0x8a90d13f3da8785a, 0x7e4fb96c1be8b67d, 0x196c1ba4cae9ed81], + [0x899aed76f0ca8d2b, 0x43b89cde0af50dd8, 0x805ea21e5951e13b, 0xe210daa428413043], + [0x6615bb542d2d8163, 0x37be4a1e5db03d95, 0xc51b56924fc47762, 0xb994ca42d142931d], + [0xe538a9b6ee93409c, 0xd82429a14a6b38da, 0x1488770da5c215b1, 0x4ade1f8e891d7658], + [0x4db682870409c304, 0x08fb9622ebf37af4, 0x677003ecf6abdff4, 0xe6b2e8723fb7cc37], + [0xfdcc6096c95fe575, 0xff0e08d72351dec6, 0xa3323ff5bb6a5b28, 0x2caa2dae89f7a2ab], + [0xaea1b45f3b46b949, 0x4231462355f753e0, 0xd59ab00bb09991fa, 0xee05650d0ae0c8d7], + [0x98aa01f5a416fd87, 0x84c3270b781ec427, 0x37680f04021034b2, 0xeb90fe3c654bf735], + [0x1ca27af8c04780b7, 0x14ef08452465867d, 0xb45c18872feefe38, 0x7c4d96bc5d8730e9], + [0xfec24c145ac49ea5, 0xc20c56906b1a32ae, 0xeaef7b4e345fa335, 0xb4c9655d4077475f], + [0x559948a78f685647, 0xe14ebcf683a56574, 0x1a6066327a77db0f, 0xf49d838f0892ce93], + [0xff0a8a791d4b6ef7, 0x02344dffb2aa2f47, 0x1726d704357a0681, 0x4ce6bb77c1bc85f4], + [0x6997e3eaf5c49a61, 0x8f4ff372b1a4dc68, 0xbea7ce04c95c2db2, 0x2accb4f49d10f761], + [0xab12d90fbfd92fb9, 0x2cb9b9b3a185ae46, 0x2a0c7a7e9ce6f49f, 0x531f307fb48f21f2], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::p256::P256FieldElement; + use crate::p256_point::P256JacobianPoint; + + const G_X: [u64; 4] = + [0xf4a13945d898c296, 0x77037d812deb33a0, 0xf8bce6e563a440f2, 0x6b17d1f2e12c4247]; + const G_Y: [u64; 4] = + [0xcbb6406837bf51f5, 0x2bce33576b315ece, 0x8ee7eb4a7c0f9e16, 0x4fe342e2fe1a7f9b]; + + const WIDTH: usize = 6; + const BITS: usize = 256; + const D: usize = BITS.div_ceil(WIDTH); + const TABLE_SIZE: usize = 1 << WIDTH; + + /// Regenerates the comb table from scratch via the crate's own (independently verified) + /// point arithmetic, per this module's construction: `pow2[i] = 2^(i*D) * G`, then + /// `table[idx] = sum of pow2[b] for each bit b set in idx`. + #[test] + fn regenerated_table_matches_checked_in_constants() { + let g = P256JacobianPoint::from_affine( + P256FieldElement::from_limbs(G_X), + P256FieldElement::from_limbs(G_Y), + ); + + let mut pow2 = [g; WIDTH]; + for i in 1..WIDTH { + let mut p = pow2[i - 1]; + for _ in 0..D { + p = p.double(); + } + pow2[i] = p; + } + + let mut table = [P256JacobianPoint::INFINITY; TABLE_SIZE]; + for bit in (0..WIDTH).rev() { + let step = 1usize << bit; + let pw = pow2[bit]; + let mut i = step; + while i < TABLE_SIZE { + table[i] = table[i - step].add(&pw); + i += 2 * step; + } + } + + for (idx, point) in table.iter().enumerate() { + if idx == 0 { + assert!(point.is_infinity().to_bool(), "table[0] must be infinity"); + assert_eq!(COMB_TABLE_X[0], [0, 0, 0, 0]); + assert_eq!(COMB_TABLE_Y[0], [0, 0, 0, 0]); + } else { + let (x, y) = point + .to_affine() + .unwrap_or_else(|| panic!("regenerated table[{idx}] is unexpectedly infinity")); + assert_eq!(x.to_limbs(), COMB_TABLE_X[idx], "table[{idx}].x mismatch"); + assert_eq!(y.to_limbs(), COMB_TABLE_Y[idx], "table[{idx}].y mismatch"); + } + } + } +} diff --git a/crypto/ec/src/p256_domain.rs b/crypto/ec/src/p256_domain.rs new file mode 100644 index 000000000..561b918b9 --- /dev/null +++ b/crypto/ec/src/p256_domain.rs @@ -0,0 +1,16 @@ +//! P-256 domain parameters, NIST SP 800-186 (Feb 2023) §3.2.1.3. `p` and `a = -3` live in +//! [`crate::p256`] (the `a = -3` shortcut is baked directly into +//! [`crate::p256_point::P256JacobianPoint::double`]); `n` lives in [`crate::p256_scalar`]. This +//! module holds the rest: the curve coefficient `b` and the base point `G`. + +/// The curve coefficient `b` in `y^2 = x^3 - 3x + b`, little-endian `u64` limbs. +pub const B_LIMBS: [u64; 4] = + [0x3bce3c3e27d2604b, 0x651d06b0cc53b0f6, 0xb3ebbd55769886bc, 0x5ac635d8aa3a93e7]; + +/// The base point `G`'s `x` coordinate, little-endian `u64` limbs. +pub const G_X_LIMBS: [u64; 4] = + [0xf4a13945d898c296, 0x77037d812deb33a0, 0xf8bce6e563a440f2, 0x6b17d1f2e12c4247]; + +/// The base point `G`'s `y` coordinate, little-endian `u64` limbs. +pub const G_Y_LIMBS: [u64; 4] = + [0xcbb6406837bf51f5, 0x2bce33576b315ece, 0x8ee7eb4a7c0f9e16, 0x4fe342e2fe1a7f9b]; diff --git a/crypto/ec/src/p256_extra_bits.rs b/crypto/ec/src/p256_extra_bits.rs new file mode 100644 index 000000000..c556cc272 --- /dev/null +++ b/crypto/ec/src/p256_extra_bits.rs @@ -0,0 +1,67 @@ +//! FIPS 186-5 Appendix A.4.1, "Conversion of a Bit String to an Integer mod n via Modular +//! Reduction" -- the "extra random bits" method both `bouncycastle-ecdsa`'s `keys_p256`'s key generation (Appendix +//! A.2.1) and `bouncycastle-ecdsa`'s `ecdsa_p256`'s randomised per-message secret (Appendix A.3.1) use to turn a +//! DRBG output wider than `n` into a value in `[1, n-1]` with negligible bias. +//! +//! # The algorithm, and why it needs its own reduction +//! +//! Appendix A.4.1's process, for a bit string `X` of length `l` and modulus `n`: +//! 1. `x = bits2int(X)` (§B.2.1, big-endian). +//! 2. `x = x mod (n-1)`. +//! 3. `x = x + 1`. +//! +//! SP 800-56A Rev. 3 Section 5.6.1.2.1 steps 5-6 (`d = (c mod (n - 1)) + 1`, key-pair generation +//! for key establishment) are the same computation, which is why this lives in `bouncycastle-ec` +//! rather than in the signature crate that first needed it. +//! +//! `X` here is 352 bits (44 bytes -- FIPS 186-5 Table A.2's "Recommended" output length for a +//! DRBG feeding a P-256 private key or per-message secret, `l = N + t` with `N = 256` and `t = 96 +//! >= 64`), 96 bits wider than the 4-limb (256-bit) width every other value in this workspace's +//! P-256 arithmetic is sized for. `bouncycastle_ec::p256_scalar` reduces mod `n`, not `n-1`, and +//! has no width wider than 4 limbs, so step 2's reduction is implemented here directly, from +//! scratch, rather than reused. +//! +//! # Constant time +//! +//! `X` is DRBG output destined to become a secret `d` or `k` once step 3 adds `1`, so this is +//! exactly the kind of "value derived from a private scalar" this workspace's constant-time rules +//! (see `bouncycastle_ec`'s crate docs) forbid branching on. [`reduce_wide_bits_mod_n_minus_1`] +//! is [`crate::barrett`]'s constant-time Barrett reduction with `m = n - 1`: two +//! fixed-width multiplications and three masked conditional subtractions, none of which branches +//! or indexes on `X`. (An earlier version reduced `X` one bit at a time with a doubling and a +//! masked subtraction per bit; correct, but 352 rounds of it cost more than a field inversion on +//! every key generation and randomised signature.) +//! +//! `extra_bits_tests.rs` pins known answers computed in Python and cross-checks the reduction +//! against that bit-serial algorithm, kept there as an independent reference, over pseudorandom +//! inputs of every width up to the maximum. + +use crate::barrett; +use crate::nat; +use crate::p256_scalar::{N_LIMBS, P256Scalar}; + +/// `n - 1`, little-endian `u64` limbs. `N_LIMBS[0]` is odd (SP 800-186 §3.2.1.3's `n`), so the +/// subtraction never borrows out of the low limb. +const N_MINUS_1_LIMBS: [u64; 4] = [N_LIMBS[0] - 1, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]]; + +/// `floor(2^512 / (n-1)) - 2^256`: the low limbs of Barrett's `mu` for `m = n - 1` (whose top +/// limb is `mu`'s only other bit -- see [`crate::barrett`]), computed in Python from +/// SP 800-186 §3.2.1.3's `n`. +const MU_LOW_LIMBS: [u64; 4] = + [0x012ffd85eedf9bff, 0x43190552df1a6c21, 0xfffffffeffffffff, 0x00000000ffffffff]; + +/// FIPS 186-5 Appendix A.4.1 steps 3-5 for `n` = the curve order: reduces the big-endian bit string +/// `bytes` modulo `n-1` and adds `1`, landing in `[1, n-1]`. `bytes` may be up to `63` bytes +/// -- `2^504 < (n-1) * 2^256`, Barrett's precondition -- which covers the 44-byte DRBG +/// output `bouncycastle-ecdsa` draws with room to spare; a longer input is a programming error and panics. +pub fn reduce_wide_bits_mod_n_minus_1(bytes: &[u8]) -> P256Scalar { + assert!( + bytes.len() < 64, + "reduce_wide_bits_mod_n_minus_1 given {} bytes, more than 63", + bytes.len() + ); + let t = barrett::limbs_from_be_bytes::<8>(bytes); + let reduced = barrett::reduce::<4, 8, 5>(&t, &N_MINUS_1_LIMBS, &MU_LOW_LIMBS); + let (plus_one, _) = nat::add(&reduced, &[1, 0, 0, 0]); + P256Scalar::from_limbs(plus_one) +} diff --git a/crypto/ec/src/p256_mul.rs b/crypto/ec/src/p256_mul.rs new file mode 100644 index 000000000..36a428aba --- /dev/null +++ b/crypto/ec/src/p256_mul.rs @@ -0,0 +1,97 @@ +//! Constant-time variable-base scalar multiplication: `[k]Q` for a secret `k` and a point `Q` +//! that is not the base point -- a peer's public key, for the ECC CDH primitive of SP 800-56A +//! Rev. 3 Section 5.7.1.2 (`P = h d_A Q_B`). +//! +//! The crate's other two multipliers do not cover this case: [`crate::p256_comb`] is fixed-base +//! (`[k]G` from a compile-time table of multiples of `G`), and [`crate::p256_wnaf`] takes +//! [`crate::p256_scalar::P256PublicScalar`] and is variable-time by design. This one takes +//! [`P256Scalar`], the secret type, and is branch-free in `k` throughout. +//! +//! # Fixed window, width 4 +//! +//! `[k]Q` is computed left to right over `ROUNDS = ceil(BITS / 4)` windows of 4 bits: each round +//! doubles the accumulator four times and adds `[w]Q` for the round's window value `w`, read off +//! `k`'s limbs at positions fixed by the loop (so *which* bits are read is public, only their +//! values are secret). `[w]Q` comes from a table of `[0]Q .. [15]Q` built per call in Jacobian +//! coordinates -- entry 0 is the identity, the rest by doubling and adding `Q` -- and is read by +//! `table_lookup`, which scans every entry under a mask rather than indexing by `w`, as the +//! comb's lookup does. The accumulator's addition is the complete [`P256JacobianPoint::add`]: it +//! masks in the identity, same-point and opposite-point cases, so the accumulator may be the +//! identity (it starts there and stays there through leading zero windows), a table entry may be +//! the identity (`w = 0`), and `Q` itself may be added to a multiple of `Q`; the comb's "no +//! offset" argument carries over unchanged. +//! +//! Cost: `4 * ROUNDS` doublings, `ROUNDS` full additions, `ROUNDS` masked scans of the table and +//! 14 point operations to build it: on P-256 about three times the fixed-base comb (whose table +//! is precomputed and whose additions are mixed) and 1.3 times the variable-time wNAF multiplier +//! (136 us against 44 us and 106 us in `p256_point_benches` / `p256_scalar_benches`). The table +//! is `16 * size_of::()` bytes of stack. Width 4 over width 5 keeps +//! that table at 1.5 KiB here and 3.4 KiB on P-521 for one fewer addition in five; a Montgomery +//! ladder would need an addition per bit. The table and the working accumulator are scrubbed +//! before returning; the caller owns the result and scrubs it when it is done. +//! +//! Verified against [`crate::p256_comb`] (for `Q = G`), against [`crate::p256_wnaf`] and against +//! `[a k mod n]G` (for `Q = [a]G`) in `p256_mul_tests.rs`, including `k = 0`, `k = 1`, `k = n-1`, +//! `Q` the identity, `Q` negated and `Q` in a non-canonical-`Z` representation. + +use crate::p256_point::{P256JacobianPoint, select_point}; +use crate::p256_scalar::P256Scalar; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 4; +const BITS: usize = 256; +const ROUNDS: usize = BITS.div_ceil(WIDTH); +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]Q`, branch-free in `k`. `Q` may be any point, including the identity; `k` may be any +/// value in `[0, n-1]`, and `[0]Q` is the identity. +pub fn multiply_point(k: &P256Scalar, q: &P256JacobianPoint) -> P256JacobianPoint { + let mut table = [P256JacobianPoint::INFINITY; TABLE_SIZE]; + table[1] = *q; + for i in 2..TABLE_SIZE { + // Which entry is built how depends only on the (public) index, never on `k` or `Q`. + table[i] = if i % 2 == 0 { table[i / 2].double() } else { table[i - 1].add(q) }; + } + + let limbs = k.limbs(); + let mut r = P256JacobianPoint::INFINITY; + for round in 0..ROUNDS { + let low_bit = (ROUNDS - 1 - round) * WIDTH; + let window = window_at(limbs, low_bit); + for _ in 0..WIDTH { + r = r.double(); + } + let mut entry = table_lookup(&table, window); + r = r.add(&entry); + entry.zeroize(); + } + + for entry in table.iter_mut() { + entry.zeroize(); + } + r +} + +/// The `WIDTH` bits of `limbs` from bit `low_bit` up, as a window value in `0..TABLE_SIZE`. Bits +/// at or above `BITS` read as zero, so on a curve whose `BITS` is not a multiple of `WIDTH` +/// (P-521) the top window sees only the scalar. The bit positions are public; the shifts and +/// masks are branch-free in the limb values. +fn window_at(limbs: &[u64; 4], low_bit: usize) -> usize { + let mut window = 0usize; + for j in (low_bit..low_bit + WIDTH).rev() { + let bit = if j < BITS { (limbs[j / 64] >> (j % 64)) & 1 } else { 0 }; + window = (window << 1) | (bit as usize); + } + window +} + +/// Table entry `secret_index`, selected by scanning every entry under a mask so the index never +/// steers which memory is read. +fn table_lookup(table: &[P256JacobianPoint; TABLE_SIZE], secret_index: usize) -> P256JacobianPoint { + let mut r = P256JacobianPoint::INFINITY; + for (i, entry) in table.iter().enumerate() { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + r = select_point(is_this_entry, entry, &r); + } + r +} diff --git a/crypto/ec/src/p256_point.rs b/crypto/ec/src/p256_point.rs new file mode 100644 index 000000000..4e59f282d --- /dev/null +++ b/crypto/ec/src/p256_point.rs @@ -0,0 +1,387 @@ +//! Branch-free Jacobian-coordinate point arithmetic on the P-256 curve `y^2 = x^3 - 3x + b`. +//! +//! A point `(X, Y, Z)` represents the affine point `(X/Z^2, Y/Z^3)`; `Z = 0` is the point at +//! infinity, represented canonically as `(1, 1, 0)`. +//! +//! # Formulas +//! +//! [`P256JacobianPoint::double`] is the standard `a = -3` Jacobian doubling (`M = 3(X-Z^2)(X+Z^2)`, +//! `S = 4XY^2`, `T = 8Y^4`, `X3 = M^2 - 2S`, `Y3 = M(S - X3) - T`, `Z3 = 2YZ`), and +//! [`P256JacobianPoint::add`]'s generic-case formula is the standard Jacobian addition (`H = U1 - +//! U2`, `R = S1 - S2`, `X3 = R^2 + H^3 - 2V`, `Y3 = R(V - X3) - S1*H^3`, `Z3 = H*Z1*Z2`, where `U1 +//! = X1*Z2^2`, `S1 = Y1*Z2^3`, `U2 = X2*Z1^2`, `S2 = Y2*Z1^3`, `V = H^2*U1`). +//! +//! A textbook implementation of these formulas reaches same-point and opposite-point (negation) +//! detection, and points already in affine form (`Z = 1`), through `if` branches -- on `H == 0`, +//! `R == 0`, `Z.is_one()`, etc. On a secret point (a private key's public point during signing, or +//! an intermediate value in a scalar multiplication holding a secret scalar), branching on any of +//! these is exactly the kind of secret-dependent control flow this workspace's constant-time rules +//! forbid. So [`P256JacobianPoint::add`] instead computes the +//! generic-add candidate and the doubling candidate *unconditionally*, computes the four +//! exceptional conditions (`self` infinite, `other` infinite, same point, opposite point) as +//! [`Condition`] masks, and masks the correct candidate into the result -- never branching on which +//! case applies. Verified (not checked in) against the affine group law in SP 800-186 Appendix +//! A.1.1, over precomputed points and their scaled (non-canonical-`Z`) Jacobian representations, +//! covering every exceptional case above plus the ordinary case. + +use crate::p256::P256FieldElement; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// A point on the P-256 curve in Jacobian coordinates. +#[derive(Clone, Copy, Debug)] +pub struct P256JacobianPoint { + /// `X`, the Jacobian `X` coordinate. + pub x: P256FieldElement, + /// `Y`, the Jacobian `Y` coordinate. + pub y: P256FieldElement, + /// `Z`, the Jacobian `Z` coordinate; `Z == 0` denotes the point at infinity. + pub z: P256FieldElement, +} + +impl P256JacobianPoint { + /// The point at infinity, the identity of the curve's group. + pub const INFINITY: Self = + Self { x: P256FieldElement::ONE, y: P256FieldElement::ONE, z: P256FieldElement::ZERO }; + + /// Builds a Jacobian point from affine coordinates (`Z = 1`). Does not check that `(x, y)` is + /// actually on the curve -- see [`crate::p256_sec1`] for that. + pub fn from_affine(x: P256FieldElement, y: P256FieldElement) -> Self { + Self { x, y, z: P256FieldElement::ONE } + } + + /// TRUE iff this is the point at infinity. + pub fn is_infinity(&self) -> Condition { + self.z.is_zero() + } + + /// Converts to affine `(x, y)` coordinates, or `None` for the point at infinity. Not constant + /// time (branches on [`Self::is_infinity`]): intended for output/decoding boundaries on + /// already-public points, not for use on a secret intermediate value. + pub fn to_affine(&self) -> Option<(P256FieldElement, P256FieldElement)> { + if self.is_infinity().to_bool() { + return None; + } + let z_inv = self.z.invert(); + let z_inv2 = z_inv.mul(&z_inv); + let z_inv3 = z_inv2.mul(&z_inv); + Some((self.x.mul(&z_inv2), self.y.mul(&z_inv3))) + } + + /// `-self`: negating the `Y` coordinate, per SP 800-186 Appendix A.1.1 (`-P = (x, -y)`). + pub fn negate(&self) -> Self { + Self { x: self.x, y: self.y.negate(), z: self.z } + } + + /// Overwrites all three coordinates with zero (which is not the identity's encoding), for + /// scrubbing a secret-derived point before it goes out of scope; see + /// [`P256FieldElement::zeroize`]. + pub fn zeroize(&mut self) { + self.x.zeroize(); + self.y.zeroize(); + self.z.zeroize(); + } + + /// `2 * self`, via the curve's `a = -3` Jacobian doubling shortcut. Total: correct (and, since + /// squaring/multiplying `0` is well-defined, does not panic) even when `self` is the point at + /// infinity, though the result is only meaningful when it is not (callers needing the + /// infinity case handled select for it separately, as [`Self::add`] does). + pub fn double(&self) -> Self { + let y1_squared = self.y.mul(&self.y); + let t = y1_squared.mul(&y1_squared); // Y1^4 + let z1_squared = self.z.mul(&self.z); + let t1 = self.x.sub(&z1_squared); + let m = self.x.add(&z1_squared); + let m = m.mul(&t1); // (X1 - Z1^2)(X1 + Z1^2) = X1^2 - Z1^4 + let m = m.add(&m).add(&m); // 3(X1^2 - Z1^4) == 3*X1^2 + a*Z1^4 for a == -3 + let s = y1_squared.mul(&self.x); + let s = s.add(&s).add(&s).add(&s); // 4*X1*Y1^2 + let t8 = t.add(&t).add(&t).add(&t).add(&t).add(&t).add(&t).add(&t); // 8*Y1^4 + let x3 = m.mul(&m).sub(&s).sub(&s); + let y3 = s.sub(&x3); + let y3 = y3.mul(&m).sub(&t8); + let z3 = self.y.add(&self.y).mul(&self.z); + Self { x: x3, y: y3, z: z3 } + } + + /// The generic (`self != ±other`, neither infinite) Jacobian addition formula, computed + /// unconditionally, plus the `H` and `R` intermediates [`Self::add`] uses to detect the + /// exceptional cases: `H == 0` iff `self` and `other` have the same affine `x`, and among + /// those, `R == 0` iff they are the same point (needs doubling) vs `R != 0` iff they are + /// negations of each other (sum is infinity). + fn generic_add(&self, other: &Self) -> (Self, P256FieldElement, P256FieldElement) { + let z2_sq = other.z.mul(&other.z); + let u1 = self.x.mul(&z2_sq); + let s1 = self.y.mul(&other.z).mul(&z2_sq); + let z1_sq = self.z.mul(&self.z); + let u2 = other.x.mul(&z1_sq); + let s2 = other.y.mul(&self.z).mul(&z1_sq); + + let h = u1.sub(&u2); + let r = s1.sub(&s2); + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&u1); + let g_neg = g.negate(); + let acc = s1.mul(&g_neg); // -S1*H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); // R^2 - (2V - H^3) == R^2 + H^3 - 2V + let y3_temp = v.sub(&x3); + let y3 = acc.add(&y3_temp.mul(&r)); // -S1*H^3 + (V - X3)*R + let z3 = h.mul(&self.z).mul(&other.z); + + (Self { x: x3, y: y3, z: z3 }, h, r) + } + + /// `self + other`, branch-free per the module docs: computes the generic-add and doubling + /// candidates unconditionally, then masks in the correct result for whichever of the four + /// exceptional cases (either operand infinite, same point, opposite point) applies, with no + /// branch on `self` or `other`. + pub fn add(&self, other: &Self) -> Self { + let self_is_infinity = self.is_infinity(); + let other_is_infinity = other.is_infinity(); + + let (generic, h, r) = self.generic_add(other); + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + + // Lowest priority first: each later select overrides the ones before it, so the final + // (highest-priority) case applied is "other is infinity", then "self is infinity". + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for an `other` given in **affine** coordinates (`Z2 = 1`), or the identity + /// when `other_is_infinity` is TRUE (`x2`/`y2` are then ignored). The same group law and the + /// same branch-free discipline as [`Self::add`] -- generic-add and doubling candidates computed + /// unconditionally, the exceptional cases masked in -- but with `Z2 = 1` the generic formula + /// simplifies: `U1 = X1`, `S1 = Y1` and `Z3 = H * Z1`, which drops five of its sixteen field + /// multiplications. The fixed-base comb multiplier adds a table entry to its accumulator on + /// every round, and every table entry is affine or the identity, so it is the caller this + /// exists for; `pub(crate)` because a caller has to know its point really has `Z = 1`. + pub(crate) fn add_affine( + &self, + x2: &P256FieldElement, + y2: &P256FieldElement, + other_is_infinity: Condition, + ) -> Self { + let self_is_infinity = self.is_infinity(); + + // The generic mixed addition, computed unconditionally: `U1 = X1`, `S1 = Y1`. + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2 + let r = self.y.sub(&s2); // S1 - S2 + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); // R^2 + H^3 - 2V + let y3 = acc.add(&v.sub(&x3).mul(&r)); // -S1*H^3 + (V - X3)*R + let z3 = h.mul(&self.z); // H * Z1 * Z2 with Z2 = 1 + let generic = Self { x: x3, y: y3, z: z3 }; + + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + let other = Self { x: *x2, y: *y2, z: P256FieldElement::ONE }; + + // Same priority order as `add`: the identity cases override everything else. + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, &other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for points that are **public**, branching on the exceptional cases instead + /// of computing every candidate and masking. + /// + /// [`Self::add`] must evaluate the doubling candidate on every call, because on secret inputs + /// it cannot branch on whether the doubling case applies -- that costs a full point doubling + /// (roughly a third of the addition) on every addition, whether or not it is ever used. This + /// version pays it only when the operands really are the same point, which for a scalar + /// multiplier over distinct precomputed multiples is essentially never. + /// + /// Restricted to `pub(crate)` and used only by [`crate::p256_wnaf`], whose scalars are + /// [`crate::p256_scalar::P256PublicScalar`] and whose points are a signer's public key and + /// the curve's own base point: every value it branches on is already known to an attacker. Do + /// not call this on anything derived from a private key or a per-message secret -- the + /// constant-time [`Self::add`] exists for that, and the scalar type split is what keeps the + /// two multipliers from being confused for one another. + pub(crate) fn add_vartime(&self, other: &Self) -> Self { + if self.is_infinity().to_bool() { + return *other; + } + if other.is_infinity().to_bool() { + return *self; + } + + let (generic, h, r) = self.generic_add(other); + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + generic + } + /// `self + (x2, y2)` for a **public**, affine `other` (`Z2 = 1`): [`Self::add_vartime`]'s + /// early-exit structure over [`Self::add_affine`]'s mixed formula, for the variable-time + /// multiplier's fixed side, whose precomputed multiples of `G` are stored affine. + /// `pub(crate)` for both of the reasons its two parents are. + pub(crate) fn add_vartime_affine(&self, x2: &P256FieldElement, y2: &P256FieldElement) -> Self { + if self.is_infinity().to_bool() { + return Self::from_affine(*x2, *y2); + } + + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2, with U1 = X1 + let r = self.y.sub(&s2); // S1 - S2, with S1 = Y1 + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); + let y3 = acc.add(&v.sub(&x3).mul(&r)); + let z3 = h.mul(&self.z); // H * Z1, with Z2 = 1 + Self { x: x3, y: y3, z: z3 } + } +} + +/// Selects `a` if `cond` is TRUE, else `b`, over every coordinate of a point. +pub(crate) fn select_point( + cond: Condition, + a: &P256JacobianPoint, + b: &P256JacobianPoint, +) -> P256JacobianPoint { + P256JacobianPoint { + x: P256FieldElement::from_internal_limbs(select_limbs( + cond, + &a.x.internal_limbs(), + &b.x.internal_limbs(), + )), + y: P256FieldElement::from_internal_limbs(select_limbs( + cond, + &a.y.internal_limbs(), + &b.y.internal_limbs(), + )), + z: P256FieldElement::from_internal_limbs(select_limbs( + cond, + &a.z.internal_limbs(), + &b.z.internal_limbs(), + )), + } +} + +fn select_limbs(cond: Condition, a: &[u64; 4], b: &[u64; 4]) -> [u64; 4] { + let mut out = [0u64; 4]; + ct::conditional_select(cond, a, b, &mut out); + out +} + +// `add_vartime` is `pub(crate)` -- deliberately unreachable from outside the crate, since calling +// it on a secret point would undo the constant-time discipline [`P256JacobianPoint::add`] exists +// for -- so no integration test can reach it. Its whole contract is that it computes the same group +// law as `add`, differing only in *how* it gets there, so that is what is pinned here: agreement on +// every exceptional case, including the ones a scalar multiplier over distinct precomputed +// multiples essentially never reaches by chance (a point added to itself, and a point added to its +// own negation). QUALITY_AND_STYLE's private-function carve-out applies. +#[cfg(test)] +mod tests { + use super::*; + use crate::p256_domain::{G_X_LIMBS, G_Y_LIMBS}; + + #[test] + fn add_affine_agrees_with_add_on_every_case() { + let g = P256JacobianPoint::from_affine( + P256FieldElement::from_limbs(G_X_LIMBS), + P256FieldElement::from_limbs(G_Y_LIMBS), + ); + // A non-canonical-Z representation of G as well: `add_affine` reads `self.z`, so `self` + // must not be assumed affine even though `other` is. + let z = P256FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = P256JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + let selves = [g, g.double(), g.negate(), P256JacobianPoint::INFINITY, scaled_g]; + let others = [g, g.double(), g.negate()]; + for a in selves { + for b in others { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_affine(&bx, &by, Condition::::FALSE).to_affine(), + a.add(&b).to_affine(), + "add_affine disagrees with add" + ); + } + // The identity flag must win regardless of the coordinates passed alongside it. + assert_eq!( + a.add_affine(&g.x, &g.y, Condition::::TRUE).to_affine(), + a.to_affine(), + "add_affine with the identity flag must return self" + ); + } + } + + #[test] + fn add_vartime_affine_agrees_with_add_on_every_case() { + let g = P256JacobianPoint::from_affine( + P256FieldElement::from_limbs(G_X_LIMBS), + P256FieldElement::from_limbs(G_Y_LIMBS), + ); + let z = P256FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = P256JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + for a in [g, g.double(), g.negate(), P256JacobianPoint::INFINITY, scaled_g] { + for b in [g, g.double(), g.negate()] { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_vartime_affine(&bx, &by).to_affine(), + a.add(&b).to_affine(), + "add_vartime_affine disagrees with add" + ); + } + } + } + + #[test] + fn add_vartime_agrees_with_add_on_every_case() { + let g = P256JacobianPoint::from_affine( + P256FieldElement::from_limbs(G_X_LIMBS), + P256FieldElement::from_limbs(G_Y_LIMBS), + ); + // G, 2G, -G and the identity cover all four of `add`'s exceptional cases pairwise, plus + // the ordinary one (e.g. G + 2G). + let points = [g, g.double(), g.negate(), P256JacobianPoint::INFINITY]; + for a in points { + for b in points { + assert_eq!( + a.add_vartime(&b).to_affine(), + a.add(&b).to_affine(), + "add_vartime disagrees with add" + ); + } + } + } +} diff --git a/crypto/ec/src/p256_scalar.rs b/crypto/ec/src/p256_scalar.rs new file mode 100644 index 000000000..09545557f --- /dev/null +++ b/crypto/ec/src/p256_scalar.rs @@ -0,0 +1,473 @@ +//! The P-256 scalar field, arithmetic mod the curve order `n` (SP 800-186 §3.2.1.3), needed for +//! `k⁻¹` and `s = k⁻¹(e + rd) mod n` in ECDSA. +//! +//! Unlike `p`, `n` is not a Solinas prime -- there is no special shape to exploit for reduction -- +//! so this uses Montgomery arithmetic instead: [`P256ScalarField`] holds its value as `x*R mod n` +//! internally (`R = 2^256`), which turns multiplication mod `n` into one wide multiply plus one +//! *fixed-length* reduction pass (REDC) with no data-dependent trial subtraction. Addition and +//! subtraction don't need REDC at all -- Montgomery form is linear, so `(a*R mod n) + (b*R mod n) +//! mod n == (a+b)*R mod n` -- and reuse the same "modulus close to `2^256`" trick +//! [`crate::p256::P256FieldElement`] uses for `p` (this one isn't Solinas-specific either: it only +//! needs `n < 2^256`, true of the curve order for every curve in NIST SP 800-186's recommended +//! set). +//! +//! # REDC +//! +//! [`redc`] is the textbook "Separated Operand Scanning" (SOS) Montgomery reduction (Koç, Acar & +//! Kaliski, *Analyzing and Comparing Montgomery Multiplication Algorithms*, 1996, Algorithm 2): for +//! `i` in `0..4`, compute `m = T[i] * n' mod 2^64` (`n' = -n⁻¹ mod 2^64`) and add `m*n` into `T` +//! at limb offset `i`, which zeroes `T[i]` by construction; after all 4 rounds `T`'s low 4 limbs +//! are zero and the result is `T`'s remaining limbs, reduced by at most one final subtraction of +//! `n`. Verified (not checked in) against 30,000 random trials plus the `(n-1)*(n-1)` worst case: +//! the reduction never touches beyond a 9-limb accumulator, which is what [`redc`] uses. + +use crate::nat; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +/// `n`, the order of the P-256 base point `G`, little-endian `u64` limbs (SP 800-186 §3.2.1.3). +pub const N_LIMBS: [u64; 4] = + [0xf3b9cac2fc632551, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000]; + +/// `2^256 mod n` -- both the correction constant [`P256ScalarField::add`] uses (mirroring +/// [`crate::p256::P256FieldElement::add`]) and, not by coincidence, the Montgomery representation +/// of `1` (`R mod n` where `R = 2^256`), since `2^256 mod n` *is* `R mod n`. +const R_LIMBS: [u64; 4] = + [0x0c46353d039cdaaf, 0x4319055258e8617b, 0x0000000000000000, 0x00000000ffffffff]; + +/// `R^2 mod n`, used to bring a plain value into Montgomery form: `REDC(x * R^2) = x*R mod n`. +const R_SQUARED_LIMBS: [u64; 4] = + [0x83244c95be79eea2, 0x4699799c49bd6fa6, 0x2845b2392b6bec59, 0x66e12d94f3d95620]; + +/// `-n^-1 mod 2^64`, the Montgomery reduction constant. +const N_PRIME: u64 = 0xccd1c8aaee00bc4f; + +/// `n - 2`, little-endian `u64` limbs -- the public exponent [`P256ScalarField::invert`] raises +/// its base to, per Fermat's little theorem (`n` is prime). +const N_MINUS_2_LIMBS: [u64; 4] = + [0xf3b9cac2fc63254f, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000]; + +/// An element of the P-256 scalar field, i.e. an integer mod `n`. Stored internally in Montgomery +/// form; see the module docs. +#[derive(Clone, Copy, Debug)] +pub struct P256ScalarField([u64; 4]); + +impl P256ScalarField { + /// The additive identity. `0`'s Montgomery form is `0` itself (`0 * R mod n == 0`). + pub const ZERO: Self = Self([0, 0, 0, 0]); + + /// The multiplicative identity. Its Montgomery form is `R mod n`, i.e. [`R_LIMBS`], not the + /// literal integer `1`. + pub const ONE: Self = Self(R_LIMBS); + + /// Builds a scalar-field element from little-endian `u64` limbs (an ordinary, non-Montgomery + /// value), reducing once if `limbs >= n` (the same single-conditional-subtraction argument as + /// [`crate::p256::P256FieldElement::from_limbs`] applies: `limbs < 2^256 < 2n`), then + /// converting to Montgomery form. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + Self(redc(&widening_mul(&reduced, &R_SQUARED_LIMBS))) + } + + /// Returns the canonical little-endian `u64` limbs (an ordinary, non-Montgomery value), `< n`. + pub fn to_limbs(&self) -> [u64; 4] { + let mut wide = [0u64; 8]; + wide[..4].copy_from_slice(&self.0); + redc(&wide) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + nat::is_zero(&self.0) + } + + /// `self + other mod n`. Operates directly on the Montgomery-form limbs: no REDC needed, since + /// Montgomery form is linear (see the module docs). + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = nat::add(&self.0, &other.0); + let (sum_plus_r, _) = nat::add(&sum, &R_LIMBS); + let (diff, borrow) = nat::sub(&sum, &N_LIMBS); + let mut when_no_carry = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_r, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod n`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = nat::sub(&self.0, &other.0); + let (corrected, _) = nat::add(&diff, &N_LIMBS); + let mut result = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod n`. + pub fn negate(&self) -> Self { + let (diff, _) = nat::sub(&N_LIMBS, &self.0); + let mut result = [0u64; 4]; + ct::conditional_select(self.is_zero(), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod n`, via Montgomery multiplication (widening multiply + [`redc`]). + pub fn mul(&self, other: &Self) -> Self { + Self(redc(&widening_mul(&self.0, &other.0))) + } + + /// Overwrites this value with zero, through a write the compiler may not elide. + /// + /// For the signing path, where a value of this type holds `d`, `k` or `k^-1` and would + /// otherwise stay legible on the stack after the signature is returned. This type is `Copy` + /// and carries public values too (verification's `u`, `v`, `e`, `r`, `s`), so it is not + /// wrapped in [`bouncycastle_utils::secret::Secret`] -- that would cost its `Copy`, its `const + /// ZERO`/`ONE`, and a scrub on every intermediate including the public ones. The trade is that + /// scrubbing is the caller's job, on every path out of the function; see + /// `bouncycastle_ecdsa`'s `sign_with_k` for the intended use. + /// + /// This does not reach values the compiler kept in registers or spilled itself, and is + /// defence in depth rather than a guarantee. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`widening_square`]. + /// + /// Used by the exponentiation loops ([`Self::invert`] and friends), which call it hundreds of + /// times in a row with nothing else competing for registers. Deliberately *not* used by this + /// curve's point arithmetic, which squares by calling `mul` with equal arguments: that was + /// tried and measured on P-256, and despite `square` being ~9% cheaper than `mul` in + /// isolation it made the constant-time comb multiplier consistently slower, because inlining + /// a third wide-multiply routine into that loop costs more than the saved partial products + /// return. See [`crate::p256::P256FieldElement::square`] for the numbers. + pub fn square(&self) -> Self { + Self(redc(&widening_square(&self.0))) + } + + /// Converts a secret scalar (`d` or `k`) into Montgomery form for field arithmetic (`k⁻¹`, `r*d`, + /// ...). Reads the secret's limbs once into a plain value, the same "protects storage, not + /// every derived computation" boundary [`crate::p256_comb::comb_multiply_base_point`] already + /// crosses to read `k`'s bits. + pub fn from_secret(secret: &P256Scalar) -> Self { + Self::from_limbs(*secret.limbs()) + } + + /// `self^-1 mod n`, or `0` if `self` is `0`. Fermat's little theorem (`n` is prime) over the + /// public exponent `n-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..4).rev() { + let limb = N_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } +} + +impl PartialEq for P256ScalarField { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. Comparing the Montgomery-form limbs directly is valid: the map `x -> x*R mod n` is a + /// bijection, so two elements are equal iff their Montgomery forms are. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..4 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for P256ScalarField {} + +/// Schoolbook widening multiply, identical in shape to +/// [`crate::p256`]'s private helper of the same name; see that module's docs for why this isn't +/// expressed generically over `Nat`. +fn widening_mul(a: &[u64; 4], b: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + for i in 0..4 { + let mut carry: u128 = 0; + for j in 0..4 { + let idx = i + j; + let prod = (a[i] as u128) * (b[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + let mut k = i + 4; + while k < 8 { + let s = (result[k] as u128) + carry; + result[k] = s as u64; + carry = s >> 64; + k += 1; + } + } + result +} + +/// `m * n`, one limb times the 4-limb modulus `n`, as the 5 limbs such a product can occupy. +/// +/// This is exactly what each round of [`redc`]'s SOS reduction needs, and the shape +/// [`crate::montgomery::redc`] has always used for the brainpool curves. Every hand-written scalar +/// REDC in this crate previously formed it with the general 4x4 [`widening_mul`] against a +/// zero-padded `[m, 0, ..., 0]` instead, asking for 16 limb multiplications where 4 are needed. +/// +/// Whether that padding costs anything turns out to depend entirely on the width, measured rather +/// than assumed: at four limbs the optimizer eliminates it completely (P-256 and secp256k1 time +/// identically either way), but it stops doing so by six, where the padded form measured 3.2x +/// slower per scalar multiplication for P-384 and 5.2x slower for P-521. All four are written this +/// way regardless, so the four modules read alike and none of them asks for work it does not want. +fn modulus_times_limb(m: u64) -> [u64; 5] { + let mut out = [0u64; 5]; + let mut carry: u128 = 0; + for j in 0..4 { + let prod = (m as u128) * (N_LIMBS[j] as u128) + carry; + out[j] = prod as u64; + carry = prod >> 64; + } + out[4] = carry as u64; + out +} + +/// Schoolbook squaring of a 4-limb value into its 8-limb square. +/// +/// `a * a` is symmetric: the product `a_i * a_j` appears twice for every `i != j`. Forming each of +/// those once and doubling costs `L(L+1)/2` limb multiplications -- 10 at this width against +/// the 16 [`widening_mul`] would form for the same value. +/// +/// Three passes: the off-diagonal products `a_i * a_j` for `i < j`; a doubling of the whole +/// accumulator; then the diagonal squares `a_i * a_i` added in at limb `2i`. Writing `result[i + +/// 4]` in the first pass is an assignment rather than an accumulation because row `i` only ever +/// reaches limbs `2i + 1 ..= i + 4 - 1`, and no earlier row reaches limb `i + 4` either, so that +/// limb is still zero when the row's final carry lands on it. The doubling cannot overflow: the +/// off-diagonal sum is strictly less than `a^2 / 2`. +/// +/// Verified against Python's arbitrary-precision `**2` over 20,000 random 4-limb values plus the +/// all-ones worst case before being written here. +fn widening_square(a: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + + // Off-diagonal products, each formed once. + for i in 0..4 { + let mut carry: u128 = 0; + for j in (i + 1)..4 { + let idx = i + j; + let prod = (a[i] as u128) * (a[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + result[i + 4] = carry as u64; + } + + // Every off-diagonal product appears twice in the square. + let mut carry = 0u64; + for limb in result.iter_mut() { + let next_carry = *limb >> 63; + // `*limb << 1` always leaves bit 0 clear and `carry` is only ever 0 or 1, so the two + // operands are disjoint: mutating this `|` to `^` is an accepted equivalent, not a bug. + *limb = (*limb << 1) | carry; + carry = next_carry; + } + debug_assert_eq!(carry, 0, "doubling the off-diagonal sum overflowed 8 limbs"); + + // Diagonal squares. + let mut carry: u128 = 0; + for i in 0..4 { + let square = (a[i] as u128) * (a[i] as u128); + let low = (result[2 * i] as u128) + ((square as u64) as u128) + carry; + result[2 * i] = low as u64; + let high = (result[2 * i + 1] as u128) + (square >> 64) + (low >> 64); + result[2 * i + 1] = high as u64; + carry = high >> 64; + } + debug_assert_eq!(carry, 0, "adding the diagonal overflowed 8 limbs"); + + result +} + +/// Montgomery reduction (SOS method): given an 8-limb value `t < n*R`, returns `t * R^-1 mod n` as +/// 4 limbs. See the module docs for the algorithm. +/// +/// After the 4 reduction rounds, the low 4 limbs of the 9-limb accumulator are zero and the +/// result -- `t * R^-1`, an integer, not yet reduced mod `n` -- occupies the remaining 5 limbs +/// (`acc[4..9]`): unlike [`crate::p256::reduce`], whose modulus is small enough that the analogous +/// step needs only one limb of headroom, `n` is close enough to `2^256` that `2n` can exceed it, +/// so a 9th limb (`acc[8]`, verified to always be `0` or `1`) is needed. The final step folds that +/// bit in exactly like [`crate::p256::P256FieldElement::add`]'s carry-aware correction: if +/// `acc[8] == 1`, the true value is `high + 2^256`, `>= 2^256 > n`, so `high + (2^256 mod n)` (i.e. +/// `+ R_LIMBS`) is the reduced result; otherwise it's `high`, conditionally reduced once more if +/// `high >= n`. +fn redc(t: &[u64; 8]) -> [u64; 4] { + let mut acc = [0u64; 9]; + acc[..8].copy_from_slice(t); + + for i in 0..4 { + let m = acc[i].wrapping_mul(N_PRIME); + let mn = modulus_times_limb(m); + + let mut carry: u128 = 0; + for j in 0..5 { + let idx = i + j; + let s = (acc[idx] as u128) + (mn[j] as u128) + carry; + acc[idx] = s as u64; + carry = s >> 64; + } + // Propagate the carry through every limb above the window, up to the accumulator's + // proven 9-limb bound. The trip count depends only on the public round index `i`, + // never on the operands: once the carry has propagated out, the remaining iterations + // add 0. A `while carry != 0` here would be a secret-dependent branch on the signing + // path. + for idx in (i + 5)..9 { + let s = (acc[idx] as u128) + carry; + acc[idx] = s as u64; + carry = s >> 64; + } + debug_assert_eq!(carry, 0, "carry must be fully propagated within the 9-limb bound"); + } + debug_assert_eq!(&acc[..4], &[0u64; 4], "REDC's low limbs must be cleared after 4 rounds"); + debug_assert!(acc[8] == 0 || acc[8] == 1, "REDC's top limb must be a single bit"); + + let high: [u64; 4] = [acc[4], acc[5], acc[6], acc[7]]; + let (high_plus_r, _) = nat::add(&high, &R_LIMBS); // used when acc[8] == 1 + let (diff, borrow) = nat::sub(&high, &N_LIMBS); // used when acc[8] == 0: is high >= n? + let mut when_no_extra_bit = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(borrow), + &high, + &diff, + &mut when_no_extra_bit, + ); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(acc[8]), + &high_plus_r, + &when_no_extra_bit, + &mut result, + ); + result +} + +fn reduce_once(limbs: [u64; 4]) -> [u64; 4] { + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut result = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut result); + result +} + +/// A scalar mod `n`, `< n`, held in `Secret` -- for a value that must never be handled in +/// non-constant time: a private key `d`, or the per-signature secret `k`. There is deliberately no +/// conversion from `P256Scalar` to [`P256PublicScalar`]: the crate's constant-time and +/// variable-time scalar multipliers take `&P256Scalar` and `&P256PublicScalar` respectively, so a +/// caller cannot pass a secret scalar to the variable-time multiplier by accident -- the type +/// system forbids it, rather than relying on a reviewer to notice. `Clone`/`Debug`/`PartialEq`/`Eq` +/// forward to [`Secret`]'s own impls (`Debug` redacting, +/// `PartialEq` constant-time), which is what `core::traits::SignaturePrivateKey`'s bound needs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct P256Scalar(Secret<[u64; 4]>); + +impl P256Scalar { + /// Builds a secret scalar from little-endian `u64` limbs, reducing once if `limbs >= n` (see + /// [`P256ScalarField::from_limbs`]'s doc for why one conditional subtraction always suffices). + pub fn from_limbs(limbs: [u64; 4]) -> Self { + let mut secret = Secret::<[u64; 4]>::new(); + *secret = reduce_once(limbs); + Self(secret) + } + + /// Builds a secret scalar from SEC 1 §2.3.7 big-endian octets, reducing once if `>= n`. + pub fn from_be_bytes(bytes: &[u8; 32]) -> Self { + Self::from_limbs(crate::p256_sec1::limbs_from_be_bytes(bytes)) + } + + /// Encodes to SEC 1 §2.3.7 big-endian octets -- the wire form of an ECDSA private key. + /// Momentarily holds the plain value in the returned array, same as any other `encode()` on a + /// `SignaturePrivateKey`: the caller asked for the bytes. + pub fn to_be_bytes(&self) -> [u8; 32] { + crate::p256_sec1::be_bytes_from_limbs(&self.0) + } + + /// The scalar's limbs, for the crate's own multiplier and field-arithmetic implementations to + /// read (see [`P256ScalarField::from_secret`]). Not exposed outside the crate: nothing outside + /// `bouncycastle-ec` should ever hold a bare, unprotected copy of a secret scalar's value. + pub(crate) fn limbs(&self) -> &[u64; 4] { + &self.0 + } +} + +/// A scalar mod `n`, `< n`, held as a plain (non-secret) value -- for a value that is public by +/// construction: the `u`, `v` scalars ECDSA verification computes from a signature and public +/// data, or a message-derived value. See [`P256Scalar`]'s docs for why there is no conversion the +/// other way. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct P256PublicScalar([u64; 4]); + +impl P256PublicScalar { + /// Builds a public scalar from little-endian `u64` limbs, reducing once if `limbs >= n`. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + Self(reduce_once(limbs)) + } + + /// Returns the scalar's little-endian `u64` limbs, `< n`. + pub fn to_limbs(&self) -> [u64; 4] { + self.0 + } + + /// `self^-1 mod n` in **variable time**, as a [`P256ScalarField`] ready for the arithmetic + /// that follows, or `0` for `0`. For ECDSA verification's `s` (FIPS 186-5 §6.4.2 step 4), + /// which is part of the signature and so already public; nothing derived from a private key + /// or a per-message secret may reach this, which is why it lives on the public scalar type + /// and not on [`P256ScalarField`], whose `invert` is the constant-time one. See + /// [`crate::inverse_vartime`]. + pub fn invert_vartime(&self) -> P256ScalarField { + P256ScalarField::from_limbs(crate::inverse_vartime::mod_inverse(&self.0, &N_LIMBS)) + } +} + +#[cfg(test)] +mod tests { + // `P256Scalar::limbs` is `pub(crate)`, deliberately not reachable from an integration test + // (nothing outside the crate should get a bare copy of a secret scalar) -- exercised here + // instead, per QUALITY_AND_STYLE's carve-out for code unreachable through the public API. + use super::*; + + #[test] + fn secret_scalar_reduces_and_round_trips() { + let limbs: [u64; 4] = [1, 2, 3, 4]; + let scalar = P256Scalar::from_limbs(limbs); + assert_eq!(*scalar.limbs(), limbs); + } + + #[test] + fn secret_scalar_reduces_out_of_range_input() { + let scalar = P256Scalar::from_limbs(N_LIMBS); + assert_eq!(*scalar.limbs(), [0, 0, 0, 0]); + } +} diff --git a/crypto/ec/src/p256_sec1.rs b/crypto/ec/src/p256_sec1.rs new file mode 100644 index 000000000..c8ffd0238 --- /dev/null +++ b/crypto/ec/src/p256_sec1.rs @@ -0,0 +1,227 @@ +//! SEC 1 v2.0 (Certicom, May 2009) §2.3.3/§2.3.4 point encoding and decoding for P-256, and the +//! public-key validation SP 800-186 (Feb 2023) Appendix D.1.1.1 requires. +//! +//! # Encodings +//! +//! Field elements are encoded big-endian, fixed-width (SEC 1 §2.3.5/§2.3.7 for `GF(p)`: "convert +//! the integer to an octet string" via §2.3.7's plain big-endian integer encoding) -- the opposite +//! byte order from this crate's internal little-endian `u64` limbs, so [`limbs_from_be_bytes`] and +//! [`be_bytes_from_limbs`] convert at the boundary. +//! +//! [`encode_uncompressed`] is SEC 1 §2.3.3 case 3 (`04 || X || Y`) and [`encode_compressed`] is +//! case 2 (`02 || X` or `03 || X`, tag selected by `y mod 2`). Both take affine coordinates +//! directly rather than a [`P256JacobianPoint`], not a `[u8; 1]` "point at infinity" case: SEC 1 +//! §2.3.3 case 1 encodes `∅` as a single `0x00` octet, but an ECDSA public key -- the only caller +//! -- can never legitimately be `∅` (SP 800-186 Appendix D.1.1.1 step 1 REJECTs it), so pushing the +//! infinity check to the type (a caller must have already gotten `Some(x, y)` out of +//! [`P256JacobianPoint::to_affine`]) makes encoding infinity a compile error instead of a +//! runtime one. +//! +//! [`decode`] is SEC 1 §2.3.4 cases 2 and 3 (again, no case 1: the single-byte infinity encoding +//! is never a valid input here, for the same reason). It performs every check SP 800-186 Appendix +//! D.1.1.1 ("Partial Public Key Validation") requires as part of decoding, not as a separate step: +//! step 1 (not infinity) holds by construction (see above), step 2 (`x`, `y` in `[0, p)`) is +//! [`limbs_less_than_p`], and step 3 (on the curve) is explicit for the uncompressed case and +//! automatic for the compressed case, since its `y` is constructed as an actual square root of +//! `x^3 + ax + b`. Appendix D.1.1.2's additional "full validation" step, `nQ = ∅`, is not needed: +//! SP 800-186 §3.2.1.3 states P-256 has order `h*n` with cofactor `h = 1`, so a point that passes +//! partial validation has order dividing the prime `n` and is not the identity, hence has order +//! exactly `n` -- `nQ = ∅` is implied, not a separate check. +//! +//! # Compressed decoding +//! +//! `p ≡ 3 (mod 4)` (SP 800-186 §3.2.1.3 gives `p`'s hex representation, which ends `...ffffffff`; +//! verified directly in this module's tests), so a square root of a residue `α` is +//! `β = α^((p+1)/4) mod p` (`β² = α^((p+1)/2) = α^((p-1)/2) * α = α` when `α` is a residue, by +//! Euler's criterion). [`SQRT_EXPONENT`] is `(p+1)/4`. [`decode`] verifies `β² == α` and rejects if +//! not: that check is what makes "α has no square root" (SEC 1 §2.3.4 step 2.4.1) detectable, since +//! the exponentiation itself is total and returns *something* whether or not `α` is a residue. +//! +//! Verified (not checked in) before any of this was written: the decompression formula against +//! 300 random points' known `y` (recomputed from `x` and `y`'s parity bit), and that a non-residue +//! `x` is correctly rejected. + +use crate::nat; +use crate::p256::{P_LIMBS, P256FieldElement}; +use crate::p256_domain::B_LIMBS; +use crate::p256_point::P256JacobianPoint; + +/// `(p + 1) / 4`, the exponent [`decode`]'s compressed-point square root raises `α` to. +const SQRT_EXPONENT: [u64; 4] = + [0x0000000000000000, 0x0000000040000000, 0x4000000000000000, 0x3fffffffc0000000]; + +/// Converts 32 big-endian bytes to little-endian `u64` limbs. +pub fn limbs_from_be_bytes(bytes: &[u8; 32]) -> [u64; 4] { + [ + u64::from_be_bytes(bytes[24..32].try_into().unwrap()), + u64::from_be_bytes(bytes[16..24].try_into().unwrap()), + u64::from_be_bytes(bytes[8..16].try_into().unwrap()), + u64::from_be_bytes(bytes[0..8].try_into().unwrap()), + ] +} + +/// Converts little-endian `u64` limbs to 32 big-endian bytes. +pub fn be_bytes_from_limbs(limbs: &[u64; 4]) -> [u8; 32] { + let mut out = [0u8; 32]; + out[0..8].copy_from_slice(&limbs[3].to_be_bytes()); + out[8..16].copy_from_slice(&limbs[2].to_be_bytes()); + out[16..24].copy_from_slice(&limbs[1].to_be_bytes()); + out[24..32].copy_from_slice(&limbs[0].to_be_bytes()); + out +} + +fn limbs_less_than_p(limbs: &[u64; 4]) -> bool { + let (_, borrow) = nat::sub(limbs, &P_LIMBS); + borrow == 1 +} + +/// `true` iff either coordinate is outside `[0, p)` (SP 800-186 Appendix D.1.1.1 step 2). +fn either_out_of_range(x_limbs: &[u64; 4], y_limbs: &[u64; 4]) -> bool { + !limbs_less_than_p(x_limbs) || !limbs_less_than_p(y_limbs) +} + +/// `y^2 == x^3 - 3x + b`. +fn is_on_curve(x: &P256FieldElement, y: &P256FieldElement) -> bool { + let lhs = y.mul(y); + let x_cubed = x.mul(x).mul(x); + let three_x = x.add(x).add(x); + let rhs = x_cubed.sub(&three_x).add(&P256FieldElement::from_limbs(B_LIMBS)); + lhs == rhs +} + +/// `base^SQRT_EXPONENT mod p`. Plain (not masked) square-and-multiply: `decode`'s only caller, +/// unlike everywhere else in this crate, only ever runs on public data (an incoming encoded +/// point), so there is nothing to protect here. +fn pow_public(base: &P256FieldElement, exponent_limbs: &[u64; 4]) -> P256FieldElement { + let mut result = P256FieldElement::ONE; + for limb_idx in (0..4).rev() { + let limb = exponent_limbs[limb_idx]; + for bit in (0..64).rev() { + result = result.square(); + if (limb >> bit) & 1 == 1 { + result = result.mul(base); + } + } + } + result +} + +/// SEC 1 §2.3.3 case 3: `04 || X || Y`, 65 bytes. +pub fn encode_uncompressed(x: &P256FieldElement, y: &P256FieldElement) -> [u8; 65] { + let mut out = [0u8; 65]; + out[0] = 0x04; + out[1..33].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out[33..65].copy_from_slice(&be_bytes_from_limbs(&y.to_limbs())); + out +} + +/// SEC 1 §2.3.3 case 2: `02 || X` or `03 || X`, 33 bytes, tag selected by `y mod 2`. +pub fn encode_compressed(x: &P256FieldElement, y: &P256FieldElement) -> [u8; 33] { + let mut out = [0u8; 33]; + out[0] = if y.to_limbs()[0] & 1 == 0 { 0x02 } else { 0x03 }; + out[1..33].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out +} + +/// Decodes and validates a SEC 1 §2.3.4 point encoding: 33 bytes (compressed, tag `0x02`/`0x03`) +/// or 65 bytes (uncompressed, tag `0x04`). See the module docs for exactly which SP 800-186 +/// Appendix D.1.1.1 checks this performs and why it constitutes full (not merely partial) +/// validation for P-256. Returns `None` for anything invalid. +pub fn decode(bytes: &[u8]) -> Option<(P256FieldElement, P256FieldElement)> { + match bytes.len() { + 33 => { + let y_tilde: u64 = match bytes[0] { + 0x02 => 0, + 0x03 => 1, + _ => return None, + }; + let x_bytes: [u8; 32] = bytes[1..33].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + if !limbs_less_than_p(&x_limbs) { + return None; + } + let x = P256FieldElement::from_limbs(x_limbs); + let x_cubed = x.mul(&x).mul(&x); + let three_x = x.add(&x).add(&x); + let alpha = x_cubed.sub(&three_x).add(&P256FieldElement::from_limbs(B_LIMBS)); + let beta = pow_public(&alpha, &SQRT_EXPONENT); + if beta.mul(&beta) != alpha { + return None; + } + let beta_parity = beta.to_limbs()[0] & 1; + let y = if beta_parity == y_tilde { beta } else { beta.negate() }; + Some((x, y)) + } + 65 => { + if bytes[0] != 0x04 { + return None; + } + let x_bytes: [u8; 32] = bytes[1..33].try_into().ok()?; + let y_bytes: [u8; 32] = bytes[33..65].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + let y_limbs = limbs_from_be_bytes(&y_bytes); + if either_out_of_range(&x_limbs, &y_limbs) { + return None; + } + let x = P256FieldElement::from_limbs(x_limbs); + let y = P256FieldElement::from_limbs(y_limbs); + if !is_on_curve(&x, &y) { + return None; + } + Some((x, y)) + } + _ => None, + } +} + +/// Decodes and validates an encoded public key (see [`decode`]) directly into a +/// [`P256JacobianPoint`]. +pub fn decode_point(bytes: &[u8]) -> Option { + let (x, y) = decode(bytes)?; + Some(P256JacobianPoint::from_affine(x, y)) +} + +// `limbs_less_than_p` and `either_out_of_range` are private and, through `decode`, only ever +// reached on inputs that also fail (or coincidentally would also fail) a later curve-membership +// check, so no integration test through the public API can pin either one's pass/fail boundary +// independent of that coincidence -- the QUALITY_AND_STYLE.md "private function, known-answer, +// can't be reached cleanly from outside the crate" exception applies to both. Known answers for +// `limbs_less_than_p`: SP 800-186 S3.2.1.3 gives `p`; 0 and `p - 1` are its immediate neighbours +// inside `[0, p)`, and `p` itself plus a value with only a non-adjacent limb bumped above `p`'s +// are its immediate neighbours outside it. `either_out_of_range` is exercised over all four +// in-range/out-of-range combinations of its two arguments, which is what pins OR against AND. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn p_is_3_mod_4() { + assert_eq!(P_LIMBS[0] & 0b11, 3); + } + + #[test] + fn limbs_less_than_p_boundary_cases() { + assert!(limbs_less_than_p(&[0, 0, 0, 0])); + + let p_minus_1 = [P_LIMBS[0] - 1, P_LIMBS[1], P_LIMBS[2], P_LIMBS[3]]; + assert!(limbs_less_than_p(&p_minus_1)); + + assert!(!limbs_less_than_p(&P_LIMBS)); + + let p_plus_2_pow_192 = [P_LIMBS[0], P_LIMBS[1], P_LIMBS[2] + 1, P_LIMBS[3]]; + assert!(!limbs_less_than_p(&p_plus_2_pow_192)); + + assert!(!limbs_less_than_p(&[u64::MAX; 4])); + } + + #[test] + fn either_out_of_range_requires_only_one_coordinate_to_be_out_of_range() { + let in_range = [0u64, 0, 0, 0]; + let out_of_range = P_LIMBS; + + assert!(!either_out_of_range(&in_range, &in_range)); + assert!(either_out_of_range(&out_of_range, &in_range)); + assert!(either_out_of_range(&in_range, &out_of_range)); + assert!(either_out_of_range(&out_of_range, &out_of_range)); + } +} diff --git a/crypto/ec/src/p256_wnaf.rs b/crypto/ec/src/p256_wnaf.rs new file mode 100644 index 000000000..f1959838e --- /dev/null +++ b/crypto/ec/src/p256_wnaf.rs @@ -0,0 +1,153 @@ +//! Interleaved windowed-NAF (wNAF) Shamir's-trick multiplication: `[u]G + [v]Q`, for ECDSA +//! verification's `R' = [u]G + [v]Q` (FIPS 186-5 §6.4.2). Both scalars and both points are public +//! -- `G` is the curve's fixed base point and `Q` a signer's public key -- so unlike +//! [`crate::p256_comb`], this multiplier takes [`P256PublicScalar`], not [`crate::p256_scalar::P256Scalar`], +//! and is written for speed, not constant time: it branches freely on the digits it computes, +//! and adds points with `add_vartime` rather than the constant-time `add` (see that method's +//! docs for why that is both sound here and worth roughly a third of every point addition). +//! +//! # wNAF +//! +//! A width-`w` NAF (non-adjacent form) represents a scalar as signed digits, each `0` or odd and +//! in `(-2^(w-1), 2^(w-1))`, with the property that any `w` consecutive digits contain at most one +//! nonzero one -- so on average only 1-in-(w+1) digits need a point addition. [`compute_wnaf`] +//! computes this the standard way: while the running value is nonzero, if it's odd, take +//! `digit = value mod 2^w`, recentre it into `(-2^(w-1), 2^(w-1)]` if it's `>= 2^(w-1)`, and +//! subtract it off (a small, `< 2^w`, signed adjustment -- a single-limb add or subtract of the +//! 4-limb value); then shift right by one bit regardless. Width `w = 5` is used for `Q`, giving 8 +//! precomputed odd multiples ([`ODD_MULTIPLE_COUNT`]) built per call. +//! +//! `G` is the curve's constant, so its odd multiples are a compile-time table of affine points +//! ([`G_ODD_MULTIPLES_X`]/[`G_ODD_MULTIPLES_Y`], width 7 -- [`G_WIDTH`]) and every `[u]G` digit +//! is a mixed addition; only `Q`'s table (width 5) is built per call. +//! +//! [`shamir_multiply`] runs both scalars' wNAF digit sequences through the same doubling loop +//! (Shamir's trick): one doubling per digit position serves both `[u]G` and `[v]Q` at once, adding +//! in whichever precomputed odd multiple (or its negation) each sequence's digit at that position +//! calls for. +//! +//! Verified (not checked in) two ways before any of this was written: the digit encoding, against +//! 2000 random `k` (does `sum(digit_i * 2^i) == k`, does every 5-digit window have at most one +//! nonzero entry, are all digits odd and correctly bounded); and the full multiplier, against the +//! SP 800-186 Appendix A.1.1 affine group law computed independently via repeated doubling and +//! conditional addition -- including `u = 0`, `v = 0`, both `0`, and `u`/`v = n-1`. + +use crate::nat; +use crate::p256::P256FieldElement; +use crate::p256_point::P256JacobianPoint; +use crate::p256_scalar::P256PublicScalar; +use crate::p256_wnaf_table::{G_ODD_MULTIPLES_X, G_ODD_MULTIPLES_Y}; + +/// Window for `Q`, whose odd multiples are computed per call: 8 point additions to build the +/// table, one addition per ~6 digits to use it. +const WIDTH: usize = 5; +/// Window for `G`, whose odd multiples are a compile-time table +/// ([`G_ODD_MULTIPLES_X`]/[`G_ODD_MULTIPLES_Y`]): nothing to build, so the widest window a +/// signed `i8` digit allows, giving one addition per ~8 digits instead of ~6, and every one of +/// them a mixed addition against an affine entry. +const G_WIDTH: usize = 7; +const WNAF_LEN: usize = 257; +const ODD_MULTIPLE_COUNT: usize = 1 << (WIDTH - 2); + +/// `[u]G + [v]Q`. +pub fn shamir_multiply( + u: &P256PublicScalar, + v: &P256PublicScalar, + q: &P256JacobianPoint, +) -> P256JacobianPoint { + let du = compute_wnaf(u.to_limbs(), G_WIDTH); + let dv = compute_wnaf(v.to_limbs(), WIDTH); + + let table_q = odd_multiples(q); + + let mut r = P256JacobianPoint::INFINITY; + for i in (0..WNAF_LEN).rev() { + r = r.double(); + if let Some((x, y)) = lookup_signed_g(du[i]) { + r = r.add_vartime_affine(&x, &y); + } + if let Some(add_q) = lookup_signed(&table_q, dv[i]) { + r = r.add_vartime(&add_q); + } + } + r +} + +fn shr1(limbs: &[u64; 4]) -> [u64; 4] { + [ + (limbs[0] >> 1) | ((limbs[1] & 1) << 63), + (limbs[1] >> 1) | ((limbs[2] & 1) << 63), + (limbs[2] >> 1) | ((limbs[3] & 1) << 63), + limbs[3] >> 1, + ] +} + +/// The width-`width` wNAF digits of `k` (`width` at most 7, so a digit fits an `i8`), LSB-first; unused trailing entries are `0`. Not +/// constant time: only ever called on public scalars (see the module docs). +fn compute_wnaf(k_limbs: [u64; 4], width: usize) -> [i8; WNAF_LEN] { + let mut digits = [0i8; WNAF_LEN]; + let mut k = k_limbs; + let mut pos = 0; + while k != [0, 0, 0, 0] { + if k[0] & 1 == 1 { + let mut digit = (k[0] & ((1 << width) - 1)) as i16; + if digit >= 1 << (width - 1) { + digit -= 1 << width; + } + k = if digit >= 0 { + let (new_k, borrow) = nat::sub(&k, &[digit as u64, 0, 0, 0]); + debug_assert_eq!(borrow, 0); + new_k + } else { + let (new_k, carry) = nat::add(&k, &[(-digit) as u64, 0, 0, 0]); + debug_assert_eq!(carry, 0); + new_k + }; + debug_assert!(pos < WNAF_LEN, "wNAF encoding overflowed its verified bound"); + digits[pos] = digit as i8; + } + k = shr1(&k); + pos += 1; + } + digits +} + +/// The odd multiples `1*p, 3*p, 5*p, ..., (2*ODD_MULTIPLE_COUNT - 1)*p`, indexed so entry `i` +/// holds `(2i+1)*p`. +fn odd_multiples(p: &P256JacobianPoint) -> [P256JacobianPoint; ODD_MULTIPLE_COUNT] { + let double_p = p.double(); + let mut table = [*p; ODD_MULTIPLE_COUNT]; + let mut cur = *p; + for entry in table.iter_mut() { + *entry = cur; + cur = cur.add_vartime(&double_p); + } + table +} + +/// `digit * p` where `p` is the point `table` was built from ([`odd_multiples`]), or `None` for +/// digit `0`. +fn lookup_signed( + table: &[P256JacobianPoint; ODD_MULTIPLE_COUNT], + digit: i8, +) -> Option { + if digit == 0 { + None + } else if digit > 0 { + Some(table[(digit as usize - 1) / 2]) + } else { + Some(table[(-digit as usize - 1) / 2].negate()) + } +} + +/// `digit * G` for a width-[`G_WIDTH`] digit, read from the compile-time table of odd multiples +/// of `G` as affine coordinates (negating `y` for a negative digit), or `None` for digit `0`. +fn lookup_signed_g(digit: i8) -> Option<(P256FieldElement, P256FieldElement)> { + if digit == 0 { + return None; + } + let index = (digit.unsigned_abs() as usize - 1) / 2; + let x = P256FieldElement::from_limbs(G_ODD_MULTIPLES_X[index]); + let y = P256FieldElement::from_limbs(G_ODD_MULTIPLES_Y[index]); + Some(if digit > 0 { (x, y) } else { (x, y.negate()) }) +} diff --git a/crypto/ec/src/p256_wnaf_table.rs b/crypto/ec/src/p256_wnaf_table.rs new file mode 100644 index 000000000..9711efd86 --- /dev/null +++ b/crypto/ec/src/p256_wnaf_table.rs @@ -0,0 +1,105 @@ +//! The checked-in odd multiples of the base point `G` for [`crate::p256_wnaf`]'s fixed side: +//! `G_ODD_MULTIPLES_X[i]`/`G_ODD_MULTIPLES_Y[i]` are the affine coordinates of `(2i + 1) G`, for +//! `i` in `0..32`, as little-endian `u64` limbs -- the table a width-7 wNAF over `G` indexes. +//! `Q`'s table is built at run time because `Q` is the signer's key; `G` is the curve's constant, +//! so its table need not be. +//! +//! Generated in Python from the affine group law (SP 800-186 Appendix A.1.1) and SP 800-186 §3.2.1.3's `G`, +//! independently of this crate's arithmetic, and checked there by `n * G == infinity`; this +//! module's own test regenerates every entry with the crate's point arithmetic and compares. + +/// `x` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_X: [[u64; 4]; 32] = [ + [0xf4a13945d898c296, 0x77037d812deb33a0, 0xf8bce6e563a440f2, 0x6b17d1f2e12c4247], + [0xfb41661bc6e7fd6c, 0xe6c6b721efada985, 0xc8f7ef951d4bf165, 0x5ecbe4d1a6330a44], + [0x21554a0dc3d033ed, 0xef8c82fd1f5be524, 0xd784c85608668fdf, 0x51590b7a515140d2], + [0x300628703187b2a3, 0x7ef9f8b8a80fef5b, 0x25bb30667c01fb60, 0x8e533b6fa0bf7b46], + [0xd79e8a4b90949ee0, 0x9e0acb8c2c6df8b3, 0x878938d51d71f872, 0xea68d7b6fedf0b71], + [0x433391d374bc21d1, 0x16742ed0255048bf, 0x0638379db0c21cda, 0x3ed113b7883b4c59], + [0x98e15d9d46072c01, 0x792e284b65ead58a, 0x61805df2d85ee2fc, 0x177c837ae0ac495a], + [0x63668c63e59b9d5f, 0xae03af92de3a0ef1, 0xadfb378999888265, 0xf0454dc6971abae7], + [0xba1abce34738a73e, 0x5fa68678f0d64af8, 0x9c0984b66f75301a, 0x47776904c0f1cc3a], + [0xc1fc7b74ab03ed83, 0x782c452257884895, 0xce39b7c17108c507, 0xcb6d2861102c0c25], + [0xfd76364e67399e83, 0x3a582139f42b1523, 0x2e4ac86eb473bca5, 0x3250fcf686637c7b], + [0x672e573045ca7896, 0x3c0bc0a5df64a4fe, 0xd28a3e39d4583fa6, 0x0e91c7239c2640d7], + [0x84a4dc45f200d687, 0x41652fc5b76f1b24, 0x85f4f52d8c07fa84, 0x3a67e2554b0c0bb6], + [0xf2e201173b0883d1, 0x576355bd683e54ab, 0xdeba2fac4611f378, 0x184ffa5819d80d51], + [0xdedd693d1c784def, 0xfd8cd1c688b58a41, 0xa7c36da090853b8c, 0xd6d33adefa195b07], + [0x3e3f9aa0a1b45b8b, 0xfac9db7d52a95b3e, 0xa85da026a7ae9aa0, 0x301d9e502dc7e05d], + [0x65c100f3cb2cd793, 0xa03b0a533aa872fd, 0xfa9aa25b89d9d34e, 0x9807d699fcd81356], + [0xa12d389033bb291a, 0x94e8e1fe92af9700, 0x8ffa3ad7326c48ca, 0xd58d4a589ed27d16], + [0x73a92894502b3348, 0xe0d21379246bfd44, 0xd6b0978611a826aa, 0x419a6a646ddb817d], + [0xa0c199ddfb2776c4, 0x547b942dd2d138d4, 0x42014976a179046e, 0x22a682f7c3996d4d], + [0x3a7de694995d2fa2, 0x6067c5c3d4175a59, 0x1cf258d2e6cfe8aa, 0x67a6bec240dee065], + [0x7544dc129b82d28d, 0x8f4bc4c6d009b30f, 0xd04230861d8f4b49, 0x986ae2506f1ff104], + [0x79c78080fae0ba03, 0x0f5f609edd29d6d9, 0x3ecd0f5ddff0672e, 0xa891d06670bde99b], + [0x51d689227b1c0d7c, 0xdd5b31583e19066d, 0x595361ea83071bbc, 0x42c315cc48958708], + [0x7d228ce6a5674455, 0x28fb7ea9758fd4fd, 0xbb22b146866e6c05, 0xf785b0e098068875], + [0x044360f0018e22b1, 0x95f7eb56e81008ff, 0xaadee6863c1d68bc, 0x672c4a514d9de43e], + [0xf126ec9f7449d036, 0x982b1ca78de9b983, 0x5a47802254b88039, 0x6f01bd49c9d95245], + [0xdec1dff7df6e60a0, 0xc2a595b762c1eada, 0x7571a109fe7fea2c, 0x079dba7ba068c926], + [0x8abd97b1d0f56077, 0x289d406e2d6c6bd8, 0x126d45a8ea907f86, 0xc116e30ebb4d2865], + [0xa2b6ea0e0faa4b45, 0xe50941119e8dc8ec, 0x765b2784fca9bdf7, 0x665f1a6ffe0c6437], + [0x5939ac380d32af0e, 0x3e7910a08b724fd5, 0x2d3a6b3d8d990001, 0x059ccb19edd3da9a], + [0x32a290825d8bdac1, 0xdf53c8af01a7cd38, 0x2a1f28a08acc7d8f, 0x6a9501d85bf5dc80], +]; + +/// `y` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_Y: [[u64; 4]; 32] = [ + [0xcbb6406837bf51f5, 0x2bce33576b315ece, 0x8ee7eb4a7c0f9e16, 0x4fe342e2fe1a7f9b], + [0x9a79b127a27d5032, 0xd82ab036384fb83d, 0x374b06ce1a64a2ec, 0x8734640c4998ff7e], + [0xd1d0bb44fda16da4, 0x0d012f00d4d80888, 0x8ae1bf36bf8a7926, 0xe0c17da8904a727d], + [0xc55e1a86c1f400b4, 0x53c73633cb041b21, 0x6d069f83a6f59000, 0x73eb1dbde0331836], + [0xe85a224a4dd048fa, 0x4d714feaa4de823f, 0x87014a964a8ea0c8, 0x2a2744c972c9fce7], + [0xe2f8eefce82a3740, 0x090d04da5e9889da, 0x24c843afa4f4c68a, 0x9099209accc4c8a2], + [0x9c43bbe2efc7bfd8, 0x26ee14c3a1fb4df3, 0xa24091adb40f4e72, 0x63bb58cd4ebea558], + [0x47e59cde0d034f36, 0x2a3b21ce75b5fa3f, 0x4e6594e51f9643e6, 0xb5b93ee3592e2d1f], + [0x32f787ff71f1fcdc, 0x81b2804428d5733f, 0x6231856577648e83, 0xaa005ee6b5b95728], + [0xe39150752bcecdaa, 0xa496716e30fa3e03, 0x5c35e7100d6d6ce4, 0x58d7614b24d9ef51], + [0x15de24a071d48c09, 0x897cd3c33b566a82, 0x97b3090d1d7eb88c, 0x42e7c342667d3593], + [0x138046543140ad55, 0x7e68833575e7a5ae, 0x1a22733bb8e0bd6d, 0x5df65c3b550dba22], + [0xa9ed16b302f79324, 0x8c188af735a7618a, 0x26daf267163afb0d, 0x27d0f1872f1fcf43], + [0x20d242c260906e6f, 0x45bdeccc63f04916, 0xa4c6d90826cb9995, 0xc0a66e276688f359], + [0x550c124593d1bca6, 0x09a166ab4b95eded, 0x3f78245f558a5dcb, 0x84aaba16ee195d7e], + [0xd58db6aea17ee267, 0x298d9ae46887ca61, 0xe0d23c026b017d72, 0x6551b6f6b3061223], + [0x2f6bf92479634af4, 0xffe630b96c587853, 0x86a01a4d1d091b2f, 0xc2a59cdccab11bf2], + [0xa5b0c9c6f586b9d5, 0x67271c163b034979, 0x76ea92632dc7fef6, 0xd45514d102726b85], + [0xdb1d6c81b09214b2, 0x13c6d072f3dee1e2, 0x545c9fb1954c2fd5, 0x332544cf1102f584], + [0x5347f649cbaa285d, 0x979dcc310265b068, 0xb918c9835a54356c, 0x4f4606b0102223ee], + [0x49c24ce1441feed5, 0x1542c7ee209aca6c, 0x6c249b49464d4499, 0xde692b7022d13158], + [0x25110c441bb07e97, 0xd86fc6289c189f25, 0xe328a4d97d3c7b61, 0x003cccc0a6460e0a], + [0xefc3edc8166934ae, 0x1c6b38f0feb0f2cc, 0x419a88c4033c1ce7, 0xb596cd922cbfa1c1], + [0xd6c4a72bb2f9b1b9, 0x74f1a1e1eb87f164, 0x2914d1dfbb7a7990, 0x649a61ce571b9585], + [0xe7bc490c10d62408, 0x4b04b6fd5f3aa60a, 0xe15c767f0d9f5b41, 0x73fdb0bf6080da6e], + [0x9935399191f37104, 0x136246589704d941, 0x611de5a4ace203f7, 0x548c7e9196a25bfe], + [0x360233dd989e17db, 0xa78551bfc3749b08, 0x11a0f21a608776ce, 0x1562080ff1d5deab], + [0xfb0da5aeb4824dea, 0x83eb2df35751a397, 0x1d223f9d2a9588ab, 0xdc1e19b743d4d181], + [0x313fd7fda410c206, 0x7d5bd5e89e59c8c5, 0xb8b16d9bb13b8765, 0xe9478823c35b30c2], + [0x6e25a6602b7f4ccf, 0x7dede5bf81e215bc, 0x6e8cca29f7eac37f, 0x490e2ca49ffd18c2], + [0x928e1e3c97fe91d1, 0x1621f7a33956cecd, 0xda65281b9345638e, 0xbb6ad7eccad49159], + [0x30aff53d5f1ef1a3, 0xf8461b5c697a6f35, 0x81c6c6e44a3c56a3, 0xca640ad193473743], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::p256::P256FieldElement; + use crate::p256_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::p256_point::P256JacobianPoint; + + #[test] + fn table_matches_the_odd_multiples_of_g_computed_by_the_point_arithmetic() { + let g = P256JacobianPoint::from_affine( + P256FieldElement::from_limbs(G_X_LIMBS), + P256FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + let mut cur = g; + for i in 0..32 { + let (x, y) = cur.to_affine().expect("an odd multiple of G below n is never infinity"); + assert_eq!(x.to_limbs(), G_ODD_MULTIPLES_X[i], "entry {i}: x of {}G", 2 * i + 1); + assert_eq!(y.to_limbs(), G_ODD_MULTIPLES_Y[i], "entry {i}: y of {}G", 2 * i + 1); + cur = cur.add_vartime(&two_g); + } + } +} diff --git a/crypto/ec/src/p256k1.rs b/crypto/ec/src/p256k1.rs new file mode 100644 index 000000000..2c40f8eb8 --- /dev/null +++ b/crypto/ec/src/p256k1.rs @@ -0,0 +1,336 @@ +//! The secp256k1 base field, GF(p) for `p = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1`. +//! +//! Domain parameters are from SEC 2 v2 §2.4.1: the prime `p` quoted there in hex is +//! `0xffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe fffffc2f` (256 bits, most +//! significant word first). [`P_LIMBS`] below is that same value in little-endian `u64` limbs. +//! Unlike every NIST curve in this crate, secp256k1's `a = 0` (see [`crate::p256k1_point`]), not +//! `-3`; the field arithmetic here is unaffected by that (it's the same kind of Solinas-friendly +//! prime as P-256/P-384), only the point-doubling formula differs. +//! +//! # Reduction algorithm +//! +//! `2^256 = p + C` with `C = 2^32 + 2^9 + 2^8 + 2^7 + 2^6 + 2^4 + 1` (33 bits), so a 512-bit +//! product `t = hi * 2^256 + lo` is congruent mod `p` to `lo + hi * C`, a value only about 33 bits +//! wider than `lo`. The private `reduce` applies that fold twice, which leaves at most a single +//! bit above `2^256`, then folds that bit in under a mask; the bounds that make exactly those +//! steps sufficient are derived in `reduce`'s own comment. This is the only reduction of its +//! shape in the crate -- [`crate::p256`], [`crate::p384`] and [`crate::sm2`] evaluate the term +//! tables of their respective standards, and the Brainpool curves use [`crate::montgomery`] -- +//! so the argument is written out here rather than by reference to another curve. +//! +//! The masked third fold is not a formality. An earlier P-384 version of this same two-fold shape +//! `debug_assert`ed that the bit was always `0`, which held for every random trial and was wrong: +//! wycheproof's `ecdsa_secp384r1` suite found a product that sets it. `p256k1_field_tests.rs` +//! constructs and pins a product that sets it on this curve. + +use crate::nat; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// `p`, little-endian `u64` limbs (SEC 2 v2 §2.4.1). +pub const P_LIMBS: [u64; 4] = + [0xfffffffefffffc2f, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff]; + +/// `2^256 mod p = 2^32 + 2^9 + 2^8 + 2^7 + 2^6 + 2^4 + 1`, little-endian `u64` limbs. This is the +/// constant [`reduce`]'s fold uses in place of `2^256`. +const C_LIMBS: [u64; 4] = [0x00000001000003d1, 0, 0, 0]; + +/// `p - 2`, little-endian `u64` limbs -- the public exponent [`P256K1FieldElement::invert`] raises +/// its base to, per Fermat's little theorem. +const P_MINUS_2_LIMBS: [u64; 4] = + [0xfffffffefffffc2d, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff]; + +/// An element of the secp256k1 base field GF(p), always held in canonical reduced form (`< p`). +#[derive(Clone, Copy, Debug)] +pub struct P256K1FieldElement([u64; 4]); + +impl P256K1FieldElement { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0]); + + /// The multiplicative identity. + pub const ONE: Self = Self([1, 0, 0, 0]); + + /// Builds a field element from little-endian `u64` limbs, reducing once if `limbs >= p`. Any + /// `u64` limb pattern is accepted: since `limbs < 2^256 < 2*p`, a single conditional + /// subtraction always suffices to bring it into `[0, p)`. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + let (diff, borrow) = nat::sub(&limbs, &P_LIMBS); + let mut result = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut result); + Self(result) + } + + /// Returns the canonical little-endian `u64` limbs, `< p`. + pub fn to_limbs(&self) -> [u64; 4] { + self.0 + } + + /// The limbs exactly as this type stores them: the canonical value itself, so this is + /// [`Self::to_limbs`] by another name. Paired with [`Self::from_internal_limbs`] so the + /// crate's point arithmetic can select between the coordinates of two existing points without + /// re-reducing each one. + pub(crate) fn internal_limbs(&self) -> [u64; 4] { + self.0 + } + + /// Wraps limbs already in this type's stored representation (see [`Self::internal_limbs`]), + /// skipping the reduction and conversion [`Self::from_limbs`] performs. Only for values that + /// came out of another element of this type, or a constant computed in that representation: + /// the masked selects in the point arithmetic, and the comb table's infinity sentinel. Not + /// exposed, since an out-of-range value would break every equality and reduction assumption + /// downstream; the range is checked in debug builds. + pub(crate) fn from_internal_limbs(limbs: [u64; 4]) -> Self { + debug_assert_eq!( + crate::nat::sub(&limbs, &P_LIMBS).1, + 1, + "from_internal_limbs given a value >= p" + ); + Self(limbs) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + nat::is_zero(&self.0) + } + + /// Overwrites the element with zero, for scrubbing a secret-derived value before it goes out + /// of scope. Defence in depth rather than a guarantee: copies the compiler kept in registers + /// or spilled itself are not reached. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self + other mod p`. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = nat::add(&self.0, &other.0); + let (sum_plus_c, _) = nat::add(&sum, &C_LIMBS); + let (diff, borrow) = nat::sub(&sum, &P_LIMBS); + let mut when_no_carry = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_c, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod p`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = nat::sub(&self.0, &other.0); + let (corrected, _) = nat::add(&diff, &P_LIMBS); + let mut result = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod p`. + pub fn negate(&self) -> Self { + let (diff, _) = nat::sub(&P_LIMBS, &self.0); + let mut result = [0u64; 4]; + ct::conditional_select(nat::is_zero(&self.0), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod p`. + pub fn mul(&self, other: &Self) -> Self { + Self(reduce(&widening_mul(&self.0, &other.0))) + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`widening_square`]. + /// + /// Used by the exponentiation loops ([`Self::invert`] and friends), which call it hundreds of + /// times in a row with nothing else competing for registers. Deliberately *not* used by this + /// curve's point arithmetic, which squares by calling `mul` with equal arguments: that was + /// tried and measured on P-256, and despite `square` being ~9% cheaper than `mul` in + /// isolation it made the constant-time comb multiplier consistently slower, because inlining + /// a third wide-multiply routine into that loop costs more than the saved partial products + /// return. See [`crate::p256::P256FieldElement::square`] for the numbers. + pub fn square(&self) -> Self { + Self(reduce(&widening_square(&self.0))) + } + + /// `self^-1 mod p`, or `0` if `self` is `0`. Fermat's little theorem over the public + /// exponent `p-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..4).rev() { + let limb = P_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } +} + +impl PartialEq for P256K1FieldElement { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..4 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for P256K1FieldElement {} + +/// Schoolbook widening multiply of two 4-limb (256-bit) operands into an 8-limb (512-bit) +/// product. See [`crate::p256`]'s widening_mul docs for why this is duplicated per curve. +fn widening_mul(a: &[u64; 4], b: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + for i in 0..4 { + let mut carry: u128 = 0; + for j in 0..4 { + let idx = i + j; + let prod = (a[i] as u128) * (b[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + // Mutating `carry = s >> 64` here is an accepted mutant, not a bug -- see + // `crate::p521::widening_mul`'s identical tail loop for the general argument (this loop's + // carry is always 0 or 1, and a left-shifted replacement contributes nothing to any later + // `result[k']`'s low 64 bits, so it never affects the returned `result` regardless of + // input). + let mut k = i + 4; + while k < 8 { + let s = (result[k] as u128) + carry; + result[k] = s as u64; + carry = s >> 64; + k += 1; + } + } + result +} + +/// Schoolbook squaring of a 4-limb value into its 8-limb square. +/// +/// `a * a` is symmetric: the product `a_i * a_j` appears twice for every `i != j`. Forming each of +/// those once and doubling costs `L(L+1)/2` limb multiplications -- 10 at this width against +/// the 16 [`widening_mul`] would form for the same value. +/// +/// Three passes: the off-diagonal products `a_i * a_j` for `i < j`; a doubling of the whole +/// accumulator; then the diagonal squares `a_i * a_i` added in at limb `2i`. Writing `result[i + +/// 4]` in the first pass is an assignment rather than an accumulation because row `i` only ever +/// reaches limbs `2i + 1 ..= i + 4 - 1`, and no earlier row reaches limb `i + 4` either, so that +/// limb is still zero when the row's final carry lands on it. The doubling cannot overflow: the +/// off-diagonal sum is strictly less than `a^2 / 2`. +/// +/// Verified against Python's arbitrary-precision `**2` over 20,000 random 4-limb values plus the +/// all-ones worst case before being written here. +fn widening_square(a: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + + // Off-diagonal products, each formed once. + for i in 0..4 { + let mut carry: u128 = 0; + for j in (i + 1)..4 { + let idx = i + j; + let prod = (a[i] as u128) * (a[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + result[i + 4] = carry as u64; + } + + // Every off-diagonal product appears twice in the square. + let mut carry = 0u64; + for limb in result.iter_mut() { + let next_carry = *limb >> 63; + // `*limb << 1` always leaves bit 0 clear and `carry` is only ever 0 or 1, so the two + // operands are disjoint: mutating this `|` to `^` is an accepted equivalent, not a bug. + *limb = (*limb << 1) | carry; + carry = next_carry; + } + debug_assert_eq!(carry, 0, "doubling the off-diagonal sum overflowed 8 limbs"); + + // Diagonal squares. + let mut carry: u128 = 0; + for i in 0..4 { + let square = (a[i] as u128) * (a[i] as u128); + let low = (result[2 * i] as u128) + ((square as u64) as u128) + carry; + result[2 * i] = low as u64; + let high = (result[2 * i + 1] as u128) + (square >> 64) + (low >> 64); + result[2 * i + 1] = high as u64; + carry = high >> 64; + } + debug_assert_eq!(carry, 0, "adding the diagonal overflowed 8 limbs"); + + result +} + +/// Reduces an 8-limb (512-bit) value modulo `p`, per the fold described in the module docs. +fn reduce(t: &[u64; 8]) -> [u64; 4] { + let mut acc: [u64; 8] = *t; + for _ in 0..2 { + let hi: [u64; 4] = [acc[4], acc[5], acc[6], acc[7]]; + let lo: [u64; 4] = [acc[0], acc[1], acc[2], acc[3]]; + // Hand-specialising this to a 4x1 multiply (secp256k1's `C = 2^32 + 977` occupies a + // single limb, so twelve of `widening_mul`'s sixteen limb multiplications are by zero) + // was tried and measured *slower*: 17.2ns vs 16.3ns per field multiplication. `C_LIMBS` + // is a compile-time constant, so the optimizer already elides those multiplications, and + // a hand-rolled version only constrains its codegen. Leave it as the general call. + let product = widening_mul(&hi, &C_LIMBS); + let lo_extended: [u64; 8] = [lo[0], lo[1], lo[2], lo[3], 0, 0, 0, 0]; + let (sum, carry) = nat::add(&product, &lo_extended); + debug_assert_eq!(carry, 0, "secp256k1 reduction fold overflowed 512 bits"); + acc = sum; + } + // After exactly two folds the high half of `acc` is a single bit. The bounds, with `C < 2^33` + // and `acc_i = hi_i * 2^256 + lo_i` the value after fold `i`: + // + // (1) `t < 2^512` splits as `hi_0 * 2^256 + lo_0` with `hi_0, lo_0 < 2^256`, so fold 1's + // `acc_1 = lo_0 + hi_0 * C < 2^256 + 2^256 * C < 2^290`: it fits in 8 limbs (the + // `carry == 0` asserted in the loop) and its high half is `hi_1 <= C`. + // (2) fold 2's `acc_2 = lo_1 + hi_1 * C < 2^256 + C^2 < 2^256 + 2^66`, so `acc[4]` is `0` or + // `1` and `acc[5..8]` are `0`. + // (3) when `acc[4]` is `1`, `lo_2 = acc_2 - 2^256 < C^2`, so `lo_2 + C < 2^66 + 2^33 < p`: + // folding the bit in can neither carry out of 256 bits nor produce a value `>= p`. + // + // The bit is folded in under a mask rather than assumed to be `0`: it is reachable from + // canonical operands, and `p256k1_field_tests.rs` constructs a product that sets it (the + // operands, and why sampling never finds them, are derived there). Without this fold that + // product's result is short by exactly `C`, the P-384 bug the module docs describe. By (3) + // the fold itself cannot carry, which the `debug_assert` pins, so `folded < 2^256 < 2p` and + // one conditional subtraction of `p` makes it canonical. + let extra_bit = acc[4]; + let low: [u64; 4] = [acc[0], acc[1], acc[2], acc[3]]; + let mut c_or_zero = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(extra_bit), + &C_LIMBS, + &[0u64; 4], + &mut c_or_zero, + ); + let (folded, carry) = nat::add(&low, &c_or_zero); + debug_assert_eq!(carry, 0, "secp256k1 third fold carried out of 256 bits"); + let (diff, borrow) = nat::sub(&folded, &P_LIMBS); + let mut result = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &folded, &diff, &mut result); + result +} diff --git a/crypto/ec/src/p256k1_comb.rs b/crypto/ec/src/p256k1_comb.rs new file mode 100644 index 000000000..d5e3dab95 --- /dev/null +++ b/crypto/ec/src/p256k1_comb.rs @@ -0,0 +1,70 @@ +//! Fixed-base comb scalar multiplication: `[k]G` for secp256k1's base point `G`, with `k` secret. +//! Identical algorithm to [`crate::p256_comb`] -- see that module's docs for the full derivation +//! and verification methodology -- with `D * WIDTH` again not dividing `BITS` exactly (`256 / 6 = +//! 42.67`, so `D = 43`, `FULL_COMB = 258`, zero-padded), same as P-256's own case. +//! +//! Verified (not checked in) against 200 random `k` plus edge cases (`k = 1, 2, 3, n-1`), each +//! cross-checked against the standard affine group law (`a = 0`). + +use crate::p256k1::P256K1FieldElement; +use crate::p256k1_comb_table::{COMB_TABLE_X, COMB_TABLE_Y}; +use crate::p256k1_point::P256K1JacobianPoint; +use crate::p256k1_scalar::P256K1Scalar; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 6; +const BITS: usize = 256; +const D: usize = BITS.div_ceil(WIDTH); +const FULL_COMB: usize = D * WIDTH; +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]G`, branch-free in `k`. +pub fn comb_multiply_base_point(k: &P256K1Scalar) -> P256K1JacobianPoint { + let limbs = k.limbs(); + let mut r = P256K1JacobianPoint::INFINITY; + let top = (FULL_COMB - 1) as isize; + for i in 0..D { + let mut secret_index: usize = 0; + let mut j = top - i as isize; + while j >= 0 { + let bit = if (j as usize) < BITS { + (limbs[(j as usize) / 64] >> ((j as usize) % 64)) & 1 + } else { + 0 + }; + // `secret_index << 1`'s vacated low bit and `bit` (always `0` or `1`) are disjoint, so + // `|` and `^` agree here. + secret_index = (secret_index << 1) | (bit as usize); + j -= D as isize; + } + r = r.double(); + let (x, y, is_infinity) = table_lookup(secret_index); + r = r.add_affine(&x, &y, is_infinity); + } + r +} + +/// Looks up comb table entry `secret_index`, scanning every one of the [`TABLE_SIZE`] entries +/// under a mask. See [`crate::p256_comb::table_lookup`]'s docs. +fn table_lookup(secret_index: usize) -> (P256K1FieldElement, P256K1FieldElement, Condition) { + let mut x_limbs = [0u64; 4]; + let mut y_limbs = [0u64; 4]; + for i in 0..TABLE_SIZE { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + let mut next_x = [0u64; 4]; + ct::conditional_select(is_this_entry, &COMB_TABLE_X[i], &x_limbs, &mut next_x); + x_limbs = next_x; + let mut next_y = [0u64; 4]; + ct::conditional_select(is_this_entry, &COMB_TABLE_Y[i], &y_limbs, &mut next_y); + y_limbs = next_y; + } + // Entry 0 is the point at infinity; every other entry is affine (Z == 1). The accumulator's + // `add_affine` takes the identity as a flag rather than as a `Z == 0` coordinate. + let is_infinity = Condition::::is_equal(secret_index as u64, 0); + ( + P256K1FieldElement::from_internal_limbs(x_limbs), + P256K1FieldElement::from_internal_limbs(y_limbs), + is_infinity, + ) +} diff --git a/crypto/ec/src/p256k1_comb_table.rs b/crypto/ec/src/p256k1_comb_table.rs new file mode 100644 index 000000000..c3375fd0c --- /dev/null +++ b/crypto/ec/src/p256k1_comb_table.rs @@ -0,0 +1,207 @@ +//! The checked-in fixed-base comb table for `[k]G` (secp256k1's base point `G`), width 6, matching +//! this crate's other curves' choice for fields over 250 bits. See [`crate::p256k1_comb`] for the +//! multiplier that uses this table and the algorithm it implements. +//! +//! `COMB_TABLE_X[i]`/`COMB_TABLE_Y[i]` are the affine `(x, y)` coordinates of the `i`-th table +//! entry, as little-endian `u64` limbs; entry `0` is the point at infinity (`x = y = 0`, which is +//! not a valid affine coordinate pair for any point on the curve, so it's an unambiguous sentinel +//! here). +//! +//! Regenerated and compared against these checked-in values by this module's own `tests` +//! submodule below, using the same construction the (not checked in) +//! `generate_p256k1_comb_table.rs` scratch example used -- so a hand-edit or transcription error in +//! this file cannot survive `cargo test`. A unit test rather than an integration test because the +//! table is deliberately `pub(crate)`, not exposed outside the crate: see +//! [`crate::p256_comb_table`]'s docs for the same reasoning. + +pub(crate) const COMB_TABLE_X: [[u64; 4]; 64] = [ + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0x59f2815b16f81798, 0x029bfcdb2dce28d9, 0x55a06295ce870b07, 0x79be667ef9dcbbac], + [0x6048b06043ff8359, 0x46b4821dc65e7651, 0xb7d282b5c21da014, 0xa2b7b3629f7bd253], + [0xbe27d057b10fd304, 0x86960638347f3a26, 0x8cd0b2d618e4a8ad, 0x6576d5548b4d88d4], + [0x476706e4dfbfa4dc, 0xf5948a7804c85b17, 0x8392119d7adbb41f, 0xd6788590731fea19], + [0x3e73fcc8f7866196, 0x25e21c3681b3f4aa, 0x52565e809339ae07, 0x29c47eab891e3cc0], + [0xea375008edecc847, 0x309feffb5844a04c, 0x170a37e4cf58f7e0, 0xf73c12851ad31962], + [0x8f6ff9c42b7fe6b1, 0xa647b5b065ded430, 0x5d53c32629aa5f4b, 0xcea2e17263d326c5], + [0xe6847df84cf27076, 0xd89858ade7627eae, 0xfcafebe77fd9af59, 0x4d49aefd784e8158], + [0x042f7989ce279a45, 0xea8b0fa8270f23bf, 0x505c7ce5bd2623d6, 0x2c0e4587cd0123c6], + [0x0035af537f56f827, 0x8344fc81d253e9a6, 0xca8f1b6a99e92f76, 0xdcb97fc13cd4a952], + [0x355569ba16f41f0a, 0x4d1ebb05a5850c70, 0x5a95769857e55d8a, 0x2543e5f81ce7d833], + [0xe0b3a84374b45960, 0x76671c46723df5a8, 0xd2429517c61ca37f, 0xe5e08b13bb68be24], + [0xf4ac2e2178e4e9da, 0x37b8d870d33dc867, 0xb70813e439ba6ea9, 0x3d56ce047d0c0bac], + [0x8c30941ccc2a56d6, 0xa0ec8285004c17ba, 0xb54f07c0a704d6d1, 0x402d950e14fe9bf7], + [0xbd776166facfba20, 0xbda9416232b1f491, 0x25d8a1a17909d66d, 0x8fd85dd82192f380], + [0x8f763889be58ad71, 0xbb30d1f5cf9a3a20, 0x0a05fe9629de8c38, 0x7778a78c28dec3e3], + [0x92b072dd48ed1367, 0x9c02cedd3d031297, 0xfdb0a5a0b38e947e, 0x0d207580a82f6607], + [0xe1c74acab15cb0a8, 0x557e6c7059af20f2, 0x02cead8233dd830d, 0x42a4634af4baaf3f], + [0xb30f195148e82495, 0x0f7f6787980ade7a, 0xed1ed0508f7226b5, 0xc1964e0efa8c13a7], + [0xaaa028551d66e242, 0xd114895ee3e64e20, 0xa4e1409d981ff163, 0x7c636cdc59373163], + [0xf96685261b2aea68, 0x6facbc2b3fada381, 0xce134bef23cd513e, 0xc7abfc5cfa35ca7b], + [0x6eb52dd9eb777697, 0x8e30ca8755333c65, 0x2ec4adacbd496935, 0x0278107b5138c61f], + [0x30663648957b0a0d, 0xf0d9b655f7643745, 0x2a0b0c4646614891, 0x40e94e242c4e3f25], + [0x4005def415544867, 0x41133d514403863c, 0xc0e4fbdcb15f58e4, 0x5e67d6973d958a99], + [0x87fa81c735d63671, 0x64885362f2eb49a9, 0xf5eb487f3d7eb3c1, 0xf1a5eae5457b84df], + [0xda173e1ebb8c8298, 0xe4573e3aac647203, 0x2bd53450ac6e28c8, 0xfa7ea7717601ba84], + [0x76205b8f0153a230, 0xe7b7f86f20dd1a21, 0xd3ae5d6d83c0c37e, 0x5c1048a532c2827d], + [0xe19a13e934087a25, 0x6e48000d1ec217e7, 0x30646a487af20404, 0xd43e05cddbd1bc55], + [0xddc3c419710f1026, 0x946f2362ca267c4a, 0x0604b808a753c190, 0x0a34bb13fecee2e7], + [0x18dc08e5e954d499, 0x1ad0c60f3b5fc120, 0x387e34d2f97cf585, 0xddb618eba6e09ab5], + [0x4b215fcf48506a70, 0x8758bf9ae7271fac, 0xad70fba2c0cabb2b, 0x0e7ac39f1d06f3fe], + [0x5cf39944b26b64f1, 0xb7edcf28f5476d99, 0xd4cda4c62511e59d, 0x7175407f1b58f010], + [0xaef3ddcc700952ef, 0x3297f9bd53ca9141, 0x2dd28fd1553aeada, 0x1cc817b6b0ccd48e], + [0x5884159bd7721115, 0xb3664810b8e16dc1, 0xfa819d536135a62f, 0x60cac14d217ddb87], + [0x53d48500959bacad, 0x339b127a602a2a3d, 0x1448bef4e641cb81, 0xefa53f427e0dae3e], + [0x7ca3deea3dcbe5dd, 0xace67db5d03eb4fb, 0x1cc96933be39c4d5, 0xe10e89b87a56a16d], + [0x4f085199122f0f71, 0x98bff21d564b3619, 0x3c554918ea1344f7, 0x80f118a6c729f953], + [0xe43fd41476a4596c, 0xd07984ed74f4fbe9, 0xe10744cc1a03d271, 0x3fa3a9591fa88c85], + [0x5408c2045ff781ed, 0x670205a77687900e, 0x44f2847c117953b2, 0x38c5897a9789510c], + [0xcfb4a0870cc1b9ed, 0xa9dee8626b53beb2, 0x0bd8e3aed51620bc, 0x6e7f11c80980e5f2], + [0xe6a6d143fbadaf91, 0xe45af20339e47148, 0x9bc61b74d04b9c13, 0x2f92485fd26eaef4], + [0xb79b5ecfdfbb68a2, 0xf6de05e94f279bbd, 0xb906d78d7a39847d, 0x197ac92f79b928bf], + [0x5c8e2b6c99b8a0ba, 0xd2cbaabb776eafc2, 0x1d2024c2bc6bc541, 0x75b0fd5a90d0dc18], + [0x7fa42090eebf6bb7, 0xae0408813e8565b4, 0x09284cf6ae51bf84, 0x27b2b3a4e0a29511], + [0x2cd59c9daeb002a6, 0x5c2c98db8e32d04a, 0xa7909e91edf6aa05, 0x802dddc6457716dc], + [0xfbc5e2c63c1dbe36, 0x4e4390f2499a8c7d, 0x09b89e987ef771b4, 0x03179e314d2df8fa], + [0x24cdcc14c9d36995, 0xc382a77a3b97b6e6, 0x85a6d079bccdefb3, 0x7aa61648693867e2], + [0xbe35d0785ae60ba3, 0x2717bd735368c6d6, 0x20faebc11d660217, 0x91b44adb4ea4c464], + [0x315b0d5d5d76033d, 0x1725522c39a2a2e7, 0x8e1396891270c1dd, 0x97cf990e77e65bb1], + [0x2492273c8aa5cd34, 0x1c796c26aeb1ed2f, 0xe6e60b4949711f57, 0x10b2104665551826], + [0x19756a7c1499350d, 0x0ce33ac1476127b0, 0xddbd90232bec1059, 0x6fca2fe6f5cce58d], + [0x28c79f0aace757fe, 0x751914572dca79ef, 0xd1a6bbbe14761633, 0x17b832e44571386b], + [0x40b90860607e5ba7, 0x1aa584bff5c5549b, 0x57f76e5ce962d92c, 0x60d45efb2b4e9144], + [0x8ad7d399404f423d, 0x595585984ab8a5f7, 0xf714d3fa276ef53c, 0x71c441d7e3b32a5b], + [0xa1a6b0ce40aeeec1, 0x861b55978252ed26, 0x6c5f6de278eff849, 0xb0fb446d18bdaea0], + [0x0847b97e9d2b66f6, 0xfd9a06ecae0e537a, 0xfb8af82ae4121630, 0x2b5a3487e6d8f9a2], + [0x792ea92c716e7b6c, 0x91a2d0aab2c822ff, 0x39af127145e2a74b, 0xadc613ff05c8f5f6], + [0xbcee76e76f67a7fa, 0x9ed7eba393b18760, 0x2d464f09a69403a8, 0xe0f4e23fd03c30ce], + [0x886e0c3293605259, 0x78df128a8d59b90b, 0x93eba20240223094, 0x37ac7f14067bef7f], + [0x1a6d4cb959f54695, 0x2332bdb7a333feec, 0x7fc5f4d0872c146a, 0x2b3fefc1caf7a4cc], + [0x8671b79095da59e5, 0x7aa04ff60a748575, 0xc9b59947d5d6a26e, 0x9e7dee383b895a3e], + [0xf8d19d143cd79f44, 0x7bc9f6be59d0bd87, 0xfaad307731e36c60, 0x1c1e628390c75ab2], + [0x93ccad491f530fee, 0x5ae91d7ffb3b1b98, 0x142893fdba91bf45, 0x25898ad2570fba39], +]; + +pub(crate) const COMB_TABLE_Y: [[u64; 4]; 64] = [ + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0x9c47d08ffb10d4b8, 0xfd17b448a6855419, 0x5da4fbfc0e1108a8, 0x483ada7726a3c465], + [0xa2397fecfe86fec2, 0x10d10835046f3835, 0x57a937a3f71e29c9, 0x693038941695122d], + [0x3214fbf674b35a7e, 0xde91c8ff19dca53c, 0x4ba282bd7471a2cd, 0xb481e63e3a1e8c39], + [0xca7bcd6bbd3b5406, 0x6206f1c4ddc9a07c, 0x940ef5c6d21c13aa, 0x28eaa8c89d5063c4], + [0x3d9d8aa926ac3dcd, 0x3e49815b2ff10fdf, 0xd55a8dec6aca3ef4, 0x4e0d94b788b83df0], + [0x2cf714db4b5d70e2, 0x99edbedf17b6864f, 0x8c3a8a7d3e0d2581, 0x506b9e2759c6b114], + [0x7e5111e5b3cf7bd1, 0x2c157fa299c547a7, 0x884e42abc251b9e4, 0x31685db59b97d96f], + [0x6b90b66203aa781e, 0x6e0f2d1a7df4d846, 0xe723f210359ca6f0, 0xcd32fc59a10dd135], + [0xaa5491ed79858da8, 0xc881dbf3c5348ebe, 0xf45baa5c946801eb, 0xa02f612707d42762], + [0x160a4b4e87b67c3d, 0x42443f4b408c6130, 0x0a19051212c01d14, 0x2efbd169ff5d737b], + [0x50e913a00596238c, 0xef0e40312fbfc3dd, 0xc23eb566573634ad, 0x9af00533173c881f], + [0x1caf639c6990cfc6, 0xf150b8e7aabacff0, 0xe2ec209e19a76c68, 0xeae00d38392329a9], + [0x1a7205c76e005f31, 0x0b5b18920bbf0efa, 0x8ab4d9bb79d928ab, 0x425098972cb116d6], + [0x78296ec7ffd37a94, 0xbe3298e1b7a03ac1, 0x72bbc0ef07122852, 0x92eae98fa04e067c], + [0x0bf5973b1275d68d, 0xca56c7197b5b9ab6, 0x144cb34fcb3fb9e9, 0x90e00591afb2fff6], + [0x3b513fc1fd9f43ac, 0x87b38411ff24ac56, 0xf7098e12f2ff5800, 0x34626d9ab5a5b22f], + [0x97607326f693d28e, 0x4bf8e9d473d7045f, 0x249d105e7806a821, 0x7f6f578e9f2e5ae6], + [0xb2f7ccf5e0da513c, 0xf4fa5d59638fc0a9, 0x8cdc23a3a39f43ce, 0xb239264b811e89b0], + [0x248b057cddab5f2c, 0x74d4e3625ee35b01, 0x9b019bbf3b8e224c, 0x9bc3051601c21ffe], + [0x22e7130ebda86be3, 0x772062dee9c411dc, 0x3be6c1effd6a1c16, 0x7274a8e2952cc272], + [0xa1b5abd192658c1c, 0xbc85b730d19d0eb0, 0xcfc5fba029a3ccc5, 0x8758b7f138f755d9], + [0x809bd73500fc31a9, 0xd450e064907f17ba, 0xb4e626800927f99f, 0xb5fe260e280282a7], + [0x8d58f6f5a60e3e05, 0x6d731d6fe5a1d66c, 0xece08e1dbd3e84df, 0x169ee313ab745c23], + [0x410a4e8ede26e2cf, 0x292dff5f82703792, 0xe043d144d4843ba9, 0x1d22c149a61301e9], + [0x1f664b95af57dca7, 0xa394ce9c1b62afc2, 0x9a8940fea22c8191, 0x0aebc938cb8cb5b4], + [0xfd9d7678d1f4270c, 0x432bed96063fa89b, 0xd71af888eb2b23ae, 0xdb11b810c620fd3e], + [0x2cf3d4d1bc73a533, 0x91ffb64198a8b3ad, 0xbf2469c70f3e2ad0, 0x6859fc332680c891], + [0x70fefab986e439bc, 0x67f66a711320dc1c, 0xd0b7b2422483c19f, 0x0aee002558089217], + [0xbc660551837b4596, 0xd9411cfe0ee17558, 0x0c1eaf02c15f0f55, 0x1d69732ce08a903c], + [0xeb60973f0acb5dd3, 0x54abb29ed770812b, 0x8c2095c67192db95, 0x7459f30c6d221978], + [0x1455fa0e100ae7a9, 0x93464741763c7a81, 0x2d0ac5eaedcd7892, 0x2571789994c7a28d], + [0x426e7efab24234d5, 0xb01fe8b774471d2a, 0xf36d3401134cc86e, 0x43b4554344e3d550], + [0x26b1dd83127f538e, 0xcbe309dd783d6a22, 0xe444283c75033d5a, 0x1e3e58c7da85c29c], + [0x4b5e3471fb69e482, 0x5d330d63d20bcad2, 0x455ee5d26976f1d0, 0xc2feb9354e25e444], + [0xcfa2a15eca6afd2a, 0x25d7c847891f9e25, 0x07a27e70dd949df7, 0x6f5bbae1a2bb65c7], + [0xb99d50433d1806cd, 0xe8319ac5e1466a33, 0xae56fa13651b1e7a, 0x8e4cd19d4498cb19], + [0x26207c601f1a9ca2, 0x2b6624a104b6563d, 0x92af032f9dde7fed, 0x43c9408c7756af48], + [0x0d42f7164a7b42a2, 0xeb89fca430883954, 0xb1eb18b23a788f67, 0x7d47da22bc60f121], + [0x9fe387c9fd6f3968, 0xffeb48261caefd1b, 0x1b4d316423ca7311, 0x947858d56dfb3c09], + [0x28c8a20507ee8b3e, 0xd05f9ae57c8e24b9, 0xded3a615b355f0d8, 0x1498b6f13b8aca26], + [0x0b6a3795192d8926, 0x126b5cad4a7699fa, 0x1a1762337fc6f4ba, 0x20070b88f3824ca8], + [0x6b38627a08912f0e, 0x66da353bf2096e06, 0xdf136ff180f7fb94, 0xac2b3ffebdfba5dc], + [0xc09ef18e609ce2ec, 0xfbb2e1eb4031d2f6, 0xe59d734cfcf1f434, 0x3cf9a44b58bf2658], + [0xc88b67e51397ec0a, 0x7abe3db71b219c9b, 0xae64b66de3bcdb3a, 0x800e23b46942800c], + [0xc1bb3aeb20a34d02, 0x9920e08ac7fd6c58, 0xe4424fead91be4a0, 0xd46b7e27db848e62], + [0x71871abe48b5ad2b, 0x2276676f01cbb15e, 0x0fbe633243935012, 0xfa461b20b73f95cb], + [0x6fa33dc1ad4e9e90, 0x9715b2430c210b89, 0x6b1d7aee99991d1c, 0x215ea70656c3b7d6], + [0xff0217dd5eb7d71b, 0x64864f81cbacdb25, 0xfa5643d779db1649, 0xf9a2a68cc58a4774], + [0xab150e3c64d34089, 0xa427e24a0a79cd92, 0x66a8943c6eb4024e, 0x0c6f126af39bf3b1], + [0xaf42a1540c680613, 0x6f5b700c0fc8d939, 0xb14f59a27f0a41dc, 0xf5498b37092d9be4], + [0xe0f0f83a01e0f19f, 0x903cc85a3a3b24b1, 0xd1f61b64f79bb62b, 0x81bf22647b2dadf7], + [0xf0a6cf26cb5b0597, 0x92cf246aac3971a5, 0x4e6675c1c73c3d28, 0x5a7ab53644c5fcc9], + [0xac84af0e0417e3d3, 0x248e3dad0fae5b6c, 0x26ee0961e9a1346e, 0xcaad90be8ba9086c], + [0x495bd4c107388eb8, 0x164eb4d7c62bcb6d, 0x5140b98166bb2cda, 0xf642d5afe309896c], + [0xdd4c2e4ecc52cb4b, 0x614f658ca94f9a62, 0x4a02453e734823c2, 0x44573f4fcb570754], + [0x8bb94c3a07fd388f, 0x55c3d037b8a94cb3, 0x53602650faaca627, 0x5bea4f2e8e0f3281], + [0xe9d9793efb00cbf4, 0x31b7a7cc71b4d7a7, 0xb5254c04e38703c1, 0xc97f9a92f22280e9], + [0x4393857792cd776f, 0xbaf513156d650a84, 0xf7aa6c27561b50eb, 0xf4281bf2368a21be], + [0x83bbbb5dda29e74a, 0x5f455f8fa76e9b01, 0x58ba3533b5ecb4c4, 0x288e321f57c1c6bd], + [0x15d04f75709faf11, 0xeaf837acf8978b40, 0x28b6429759909228, 0x92e6323d24602fd5], + [0x3e0fba1a53ee485b, 0x354a1921026ca84f, 0xdd1ab3c30ac7cc2f, 0x49831bfa780722a4], + [0xbba3ba1e3714ff9f, 0x1e7bb20b27969b07, 0x523b17914d391133, 0xa7f4d2429c57b316], + [0x0baa59821b7180e3, 0x8a89e34cc7c54c52, 0xc9d4aad1f28203db, 0x2188b6d4b0267681], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::p256k1::P256K1FieldElement; + use crate::p256k1_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::p256k1_point::P256K1JacobianPoint; + + const WIDTH: usize = 6; + const BITS: usize = 256; + const D: usize = BITS.div_ceil(WIDTH); + const TABLE_SIZE: usize = 1 << WIDTH; + + /// Regenerates the comb table from scratch via the crate's own (independently verified) + /// point arithmetic, per this module's construction: `pow2[i] = 2^(i*D) * G`, then + /// `table[idx] = sum of pow2[b] for each bit b set in idx`. + #[test] + fn regenerated_table_matches_checked_in_constants() { + let g = P256K1JacobianPoint::from_affine( + P256K1FieldElement::from_limbs(G_X_LIMBS), + P256K1FieldElement::from_limbs(G_Y_LIMBS), + ); + + let mut pow2 = [g; WIDTH]; + for i in 1..WIDTH { + let mut p = pow2[i - 1]; + for _ in 0..D { + p = p.double(); + } + pow2[i] = p; + } + + let mut table = [P256K1JacobianPoint::INFINITY; TABLE_SIZE]; + for bit in (0..WIDTH).rev() { + let step = 1usize << bit; + let pw = pow2[bit]; + let mut i = step; + while i < TABLE_SIZE { + table[i] = table[i - step].add(&pw); + i += 2 * step; + } + } + + for (idx, point) in table.iter().enumerate() { + if idx == 0 { + assert!(point.is_infinity().to_bool(), "table[0] must be infinity"); + assert_eq!(COMB_TABLE_X[0], [0, 0, 0, 0]); + assert_eq!(COMB_TABLE_Y[0], [0, 0, 0, 0]); + } else { + let (x, y) = point + .to_affine() + .unwrap_or_else(|| panic!("regenerated table[{idx}] is unexpectedly infinity")); + assert_eq!(x.to_limbs(), COMB_TABLE_X[idx], "table[{idx}].x mismatch"); + assert_eq!(y.to_limbs(), COMB_TABLE_Y[idx], "table[{idx}].y mismatch"); + } + } + } +} diff --git a/crypto/ec/src/p256k1_domain.rs b/crypto/ec/src/p256k1_domain.rs new file mode 100644 index 000000000..2159940b7 --- /dev/null +++ b/crypto/ec/src/p256k1_domain.rs @@ -0,0 +1,15 @@ +//! secp256k1 domain parameters, SEC 2 v2 (Certicom, Jan 2010) §2.4.1. `p` lives in +//! [`crate::p256k1`] (`a = 0`, unlike every NIST curve in this crate -- see +//! [`crate::p256k1_point`]); `n` lives in [`crate::p256k1_scalar`]. This module holds the rest: the +//! curve coefficient `b` and the base point `G`. + +/// The curve coefficient `b` in `y^2 = x^3 + b` (`a = 0`), little-endian `u64` limbs. +pub const B_LIMBS: [u64; 4] = [0x0000000000000007, 0, 0, 0]; + +/// The base point `G`'s `x` coordinate, little-endian `u64` limbs. +pub const G_X_LIMBS: [u64; 4] = + [0x59f2815b16f81798, 0x029bfcdb2dce28d9, 0x55a06295ce870b07, 0x79be667ef9dcbbac]; + +/// The base point `G`'s `y` coordinate, little-endian `u64` limbs. +pub const G_Y_LIMBS: [u64; 4] = + [0x9c47d08ffb10d4b8, 0xfd17b448a6855419, 0x5da4fbfc0e1108a8, 0x483ada7726a3c465]; diff --git a/crypto/ec/src/p256k1_extra_bits.rs b/crypto/ec/src/p256k1_extra_bits.rs new file mode 100644 index 000000000..1a90aaf14 --- /dev/null +++ b/crypto/ec/src/p256k1_extra_bits.rs @@ -0,0 +1,61 @@ +//! FIPS 186-5 Appendix A.4.1, "Conversion of a Bit String to an Integer mod n via Modular +//! Reduction" -- the "extra random bits" method both `bouncycastle-ecdsa`'s `keys_p256k1`'s key generation +//! (Appendix A.2.1) and `bouncycastle-ecdsa`'s `ecdsa_p256k1`'s randomised per-message secret (Appendix A.3.1) +//! use to turn a DRBG output wider than `n` into a value in `[1, n-1]`. Identical algorithm to +//! [`crate::p256_extra_bits`] -- see that module's docs for the full reasoning -- with secp256k1's +//! own `n` substituted. +//! +//! # Why secp256k1 draws extra bits at all +//! +//! Not for bias. secp256k1's `n` is within `2^129` of `2^256`, so Appendix A.4.1 step 2's bound +//! (`2ρ(1-ρ)(n-1) <= ε·N`, `ε = 2^-64`) already holds at `l = 256` with a bias near `2^-127` +//! (secp256k1 is not a NIST curve, so there is no Table A.2 entry; the bound was evaluated +//! directly in Python against SEC 2 v2 §2.4.1's `n`). An earlier version of `bouncycastle-ecdsa` drew +//! exactly 32 bytes for both key generation and the randomised per-message secret on that basis, +//! and reduced with a single conditional subtraction of `n - 1`. +//! +//! Appendix A.3.1 is stricter than that bound, though. Its step 2 returns FAILURE if `t < 64` and +//! its step 3 says: *"Obtain a bit string of N+t returned_bits from the established DRBG"*. That +//! is a requirement on the per-message secret's draw independent of how close `n` is to a power +//! of two, and 256 bits does not meet it. So `ECDSASecp256K1` now draws `N + 64 = 320` bits (40 +//! bytes, `bouncycastle-ecdsa`'s `keys_p256k1::EXTRA_BITS_DRBG_OUTPUT_LEN`) and reduces them with this module. +//! Key generation draws the same 40 bytes: Appendix A.2.1's lengths are minimums, so the wider +//! draw is permitted there too, and one reduction path per curve is simpler than two. The extra +//! bits satisfy the requirement rather than lower the bias: for an `n` this close to a power of +//! two, `2^l mod (n-1)` grows with `l` as fast as `2^l` does, so the bias stays near `2^-127` at +//! `l = 320` too (computed, not assumed). +//! +//! The reduction itself is [`crate::barrett`]'s constant-time Barrett reduction with +//! `m = n - 1` (see [`crate::p256_extra_bits`]'s docs for the constant-time argument); `p256k1_extra_bits_tests.rs` +//! pins known answers computed in Python and cross-checks it against the bit-serial algorithm this +//! module used to implement, kept there as an independent reference. + +use crate::barrett; +use crate::nat; +use crate::p256k1_scalar::{N_LIMBS, P256K1Scalar}; + +/// `n - 1`, little-endian `u64` limbs. `N_LIMBS[0]` is odd (`n` is prime), so the subtraction +/// never borrows out of the low limb. +const N_MINUS_1_LIMBS: [u64; 4] = [N_LIMBS[0] - 1, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]]; + +/// `floor(2^512 / (n-1)) - 2^256`: the low limbs of Barrett's `mu` for `m = n - 1` (whose top +/// limb is `mu`'s only other bit -- see [`crate::barrett`]), computed in Python from +/// SEC 2 v2 §2.4.1's `n`. +const MU_LOW_LIMBS: [u64; 4] = + [0x402da1732fc9bec1, 0x4551231950b75fc4, 0x0000000000000001, 0x0000000000000000]; + +/// FIPS 186-5 Appendix A.4.1 steps 3-5 for `n` = the curve order: reduces the big-endian bit string +/// `bytes` modulo `n-1` and adds `1`, landing in `[1, n-1]`. `bytes` may be up to `63` bytes +/// -- `2^504 < (n-1) * 2^256`, Barrett's precondition -- which covers the 40-byte DRBG +/// output `bouncycastle-ecdsa` draws with room to spare; a longer input is a programming error and panics. +pub fn reduce_wide_bits_mod_n_minus_1(bytes: &[u8]) -> P256K1Scalar { + assert!( + bytes.len() < 64, + "reduce_wide_bits_mod_n_minus_1 given {} bytes, more than 63", + bytes.len() + ); + let t = barrett::limbs_from_be_bytes::<8>(bytes); + let reduced = barrett::reduce::<4, 8, 5>(&t, &N_MINUS_1_LIMBS, &MU_LOW_LIMBS); + let (plus_one, _) = nat::add(&reduced, &[1, 0, 0, 0]); + P256K1Scalar::from_limbs(plus_one) +} diff --git a/crypto/ec/src/p256k1_mul.rs b/crypto/ec/src/p256k1_mul.rs new file mode 100644 index 000000000..612fed415 --- /dev/null +++ b/crypto/ec/src/p256k1_mul.rs @@ -0,0 +1,100 @@ +//! Constant-time variable-base scalar multiplication: `[k]Q` for a secret `k` and a point `Q` +//! that is not the base point -- a peer's public key, for the ECC CDH primitive of SP 800-56A +//! Rev. 3 Section 5.7.1.2 (`P = h d_A Q_B`). +//! +//! The crate's other two multipliers do not cover this case: [`crate::p256k1_comb`] is fixed-base +//! (`[k]G` from a compile-time table of multiples of `G`), and [`crate::p256k1_wnaf`] takes +//! [`crate::p256k1_scalar::P256K1PublicScalar`] and is variable-time by design. This one takes +//! [`P256K1Scalar`], the secret type, and is branch-free in `k` throughout. +//! +//! # Fixed window, width 4 +//! +//! `[k]Q` is computed left to right over `ROUNDS = ceil(BITS / 4)` windows of 4 bits: each round +//! doubles the accumulator four times and adds `[w]Q` for the round's window value `w`, read off +//! `k`'s limbs at positions fixed by the loop (so *which* bits are read is public, only their +//! values are secret). `[w]Q` comes from a table of `[0]Q .. [15]Q` built per call in Jacobian +//! coordinates -- entry 0 is the identity, the rest by doubling and adding `Q` -- and is read by +//! `table_lookup`, which scans every entry under a mask rather than indexing by `w`, as the +//! comb's lookup does. The accumulator's addition is the complete [`P256K1JacobianPoint::add`]: it +//! masks in the identity, same-point and opposite-point cases, so the accumulator may be the +//! identity (it starts there and stays there through leading zero windows), a table entry may be +//! the identity (`w = 0`), and `Q` itself may be added to a multiple of `Q`; the comb's "no +//! offset" argument carries over unchanged. +//! +//! Cost: `4 * ROUNDS` doublings, `ROUNDS` full additions, `ROUNDS` masked scans of the table and +//! 14 point operations to build it: on P-256 about three times the fixed-base comb (whose table +//! is precomputed and whose additions are mixed) and 1.3 times the variable-time wNAF multiplier +//! (136 us against 44 us and 106 us in `p256_point_benches` / `p256_scalar_benches`). The table +//! is `16 * size_of::()` bytes of stack. Width 4 over width 5 keeps +//! that table at 1.5 KiB here and 3.4 KiB on P-521 for one fewer addition in five; a Montgomery +//! ladder would need an addition per bit. The table and the working accumulator are scrubbed +//! before returning; the caller owns the result and scrubs it when it is done. +//! +//! Verified against [`crate::p256k1_comb`] (for `Q = G`), against [`crate::p256k1_wnaf`] and against +//! `[a k mod n]G` (for `Q = [a]G`) in `p256k1_mul_tests.rs`, including `k = 0`, `k = 1`, `k = n-1`, +//! `Q` the identity, `Q` negated and `Q` in a non-canonical-`Z` representation. + +use crate::p256k1_point::{P256K1JacobianPoint, select_point}; +use crate::p256k1_scalar::P256K1Scalar; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 4; +const BITS: usize = 256; +const ROUNDS: usize = BITS.div_ceil(WIDTH); +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]Q`, branch-free in `k`. `Q` may be any point, including the identity; `k` may be any +/// value in `[0, n-1]`, and `[0]Q` is the identity. +pub fn multiply_point(k: &P256K1Scalar, q: &P256K1JacobianPoint) -> P256K1JacobianPoint { + let mut table = [P256K1JacobianPoint::INFINITY; TABLE_SIZE]; + table[1] = *q; + for i in 2..TABLE_SIZE { + // Which entry is built how depends only on the (public) index, never on `k` or `Q`. + table[i] = if i % 2 == 0 { table[i / 2].double() } else { table[i - 1].add(q) }; + } + + let limbs = k.limbs(); + let mut r = P256K1JacobianPoint::INFINITY; + for round in 0..ROUNDS { + let low_bit = (ROUNDS - 1 - round) * WIDTH; + let window = window_at(limbs, low_bit); + for _ in 0..WIDTH { + r = r.double(); + } + let mut entry = table_lookup(&table, window); + r = r.add(&entry); + entry.zeroize(); + } + + for entry in table.iter_mut() { + entry.zeroize(); + } + r +} + +/// The `WIDTH` bits of `limbs` from bit `low_bit` up, as a window value in `0..TABLE_SIZE`. Bits +/// at or above `BITS` read as zero, so on a curve whose `BITS` is not a multiple of `WIDTH` +/// (P-521) the top window sees only the scalar. The bit positions are public; the shifts and +/// masks are branch-free in the limb values. +fn window_at(limbs: &[u64; 4], low_bit: usize) -> usize { + let mut window = 0usize; + for j in (low_bit..low_bit + WIDTH).rev() { + let bit = if j < BITS { (limbs[j / 64] >> (j % 64)) & 1 } else { 0 }; + window = (window << 1) | (bit as usize); + } + window +} + +/// Table entry `secret_index`, selected by scanning every entry under a mask so the index never +/// steers which memory is read. +fn table_lookup( + table: &[P256K1JacobianPoint; TABLE_SIZE], + secret_index: usize, +) -> P256K1JacobianPoint { + let mut r = P256K1JacobianPoint::INFINITY; + for (i, entry) in table.iter().enumerate() { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + r = select_point(is_this_entry, entry, &r); + } + r +} diff --git a/crypto/ec/src/p256k1_point.rs b/crypto/ec/src/p256k1_point.rs new file mode 100644 index 000000000..78ccff5ce --- /dev/null +++ b/crypto/ec/src/p256k1_point.rs @@ -0,0 +1,373 @@ +//! Branch-free Jacobian-coordinate point arithmetic on the secp256k1 curve `y^2 = x^3 + 7` (`a = +//! 0`, unlike every NIST curve in this crate). +//! +//! [`P256K1JacobianPoint::add`]'s exceptional-case handling (infinity, same-point, opposite-point) +//! is identical to [`crate::p256_point::P256JacobianPoint::add`] -- see that module's docs for the +//! full derivation and verification methodology, which applies unchanged here. Only +//! [`P256K1JacobianPoint::double`] differs, since it cannot use the `a = -3` shortcut: this is the +//! standard `a = 0` doubling formula ("dbl-2009-l" in the hyperelliptic.org explicit-formulas +//! database: `A = X1^2`, `B = Y1^2`, `C = B^2`, `D = 2*((X1+B)^2-A-C)`, `E = 3*A`, `F = E^2`, `X3 = +//! F-2*D`, `Y3 = E*(D-X3)-8*C`, `Z3 = 2*Y1*Z1`), verified (not checked in) against the affine +//! doubling law over many random on-curve points before being ported here. + +use crate::p256k1::P256K1FieldElement; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// A point on the secp256k1 curve in Jacobian coordinates. +#[derive(Clone, Copy, Debug)] +pub struct P256K1JacobianPoint { + /// `X`, the Jacobian `X` coordinate. + pub x: P256K1FieldElement, + /// `Y`, the Jacobian `Y` coordinate. + pub y: P256K1FieldElement, + /// `Z`, the Jacobian `Z` coordinate; `Z == 0` denotes the point at infinity. + pub z: P256K1FieldElement, +} + +impl P256K1JacobianPoint { + /// The point at infinity, the identity of the curve's group. + pub const INFINITY: Self = Self { + x: P256K1FieldElement::ONE, + y: P256K1FieldElement::ONE, + z: P256K1FieldElement::ZERO, + }; + + /// Builds a Jacobian point from affine coordinates (`Z = 1`). Does not check that `(x, y)` is + /// actually on the curve -- see [`crate::p256k1_sec1`] for that. + pub fn from_affine(x: P256K1FieldElement, y: P256K1FieldElement) -> Self { + Self { x, y, z: P256K1FieldElement::ONE } + } + + /// TRUE iff this is the point at infinity. + pub fn is_infinity(&self) -> Condition { + self.z.is_zero() + } + + /// Converts to affine `(x, y)` coordinates, or `None` for the point at infinity. Not constant + /// time: see [`crate::p256_point::P256JacobianPoint::to_affine`]'s docs. + pub fn to_affine(&self) -> Option<(P256K1FieldElement, P256K1FieldElement)> { + if self.is_infinity().to_bool() { + return None; + } + let z_inv = self.z.invert(); + let z_inv2 = z_inv.mul(&z_inv); + let z_inv3 = z_inv2.mul(&z_inv); + Some((self.x.mul(&z_inv2), self.y.mul(&z_inv3))) + } + + /// `-self`: negating the `Y` coordinate. + pub fn negate(&self) -> Self { + Self { x: self.x, y: self.y.negate(), z: self.z } + } + + /// Overwrites all three coordinates with zero (which is not the identity's encoding), for + /// scrubbing a secret-derived point before it goes out of scope; see + /// [`P256K1FieldElement::zeroize`]. + pub fn zeroize(&mut self) { + self.x.zeroize(); + self.y.zeroize(); + self.z.zeroize(); + } + + /// `2 * self`, via the `a = 0` doubling formula ("dbl-2009-l") described in the module docs. + pub fn double(&self) -> Self { + let a = self.x.mul(&self.x); // A = X1^2 + let b = self.y.mul(&self.y); // B = Y1^2 + let c = b.mul(&b); // C = B^2 + let x1_plus_b = self.x.add(&b); + let d = x1_plus_b.mul(&x1_plus_b).sub(&a).sub(&c); + let d = d.add(&d); // D = 2*((X1+B)^2 - A - C) + let e = a.add(&a).add(&a); // E = 3*A + let f = e.mul(&e); // F = E^2 + let x3 = f.sub(&d).sub(&d); // X3 = F - 2*D + let c8 = c.add(&c).add(&c).add(&c).add(&c).add(&c).add(&c).add(&c); // 8*C + let y3 = e.mul(&d.sub(&x3)).sub(&c8); // Y3 = E*(D-X3) - 8*C + let z3 = self.y.add(&self.y).mul(&self.z); // Z3 = 2*Y1*Z1 + Self { x: x3, y: y3, z: z3 } + } + + /// The generic (`self != ±other`, neither infinite) Jacobian addition formula, plus the `H` + /// and `R` intermediates [`Self::add`] uses to detect the exceptional cases. See + /// [`crate::p256_point::P256JacobianPoint`]'s private `generic_add`'s docs -- this formula + /// doesn't depend on `a` at all, so it's identical to the NIST curves' version. + fn generic_add(&self, other: &Self) -> (Self, P256K1FieldElement, P256K1FieldElement) { + let z2_sq = other.z.mul(&other.z); + let u1 = self.x.mul(&z2_sq); + let s1 = self.y.mul(&other.z).mul(&z2_sq); + let z1_sq = self.z.mul(&self.z); + let u2 = other.x.mul(&z1_sq); + let s2 = other.y.mul(&self.z).mul(&z1_sq); + + let h = u1.sub(&u2); + let r = s1.sub(&s2); + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); + let v = h_squared.mul(&u1); + let g_neg = g.negate(); + let acc = s1.mul(&g_neg); + let g2 = g_neg.add(&v).add(&v); + let x3 = r.mul(&r).sub(&g2); + let y3_temp = v.sub(&x3); + let y3 = acc.add(&y3_temp.mul(&r)); + let z3 = h.mul(&self.z).mul(&other.z); + + (Self { x: x3, y: y3, z: z3 }, h, r) + } + + /// `self + other`, branch-free per [`crate::p256_point::P256JacobianPoint::add`]'s docs. + pub fn add(&self, other: &Self) -> Self { + let self_is_infinity = self.is_infinity(); + let other_is_infinity = other.is_infinity(); + + let (generic, h, r) = self.generic_add(other); + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + // Dropping the `!` here (making `is_opposite_point` collapse onto `is_same_point`) is an + // accepted mutant, not a bug: when `h == 0`, `generic_add`'s own `z3 = h*self.z*other.z` + // is already `0` regardless of `r`, so `generic` is already infinity-representing in the + // true opposite-point case (`h == 0, r != 0`) even without this override selecting + // `Self::INFINITY` explicitly -- see `crate::p521_point`'s identical case for the full + // argument. + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for an `other` given in **affine** coordinates (`Z2 = 1`), or the identity + /// when `other_is_infinity` is TRUE (`x2`/`y2` are then ignored). The same group law and the + /// same branch-free discipline as [`Self::add`] -- generic-add and doubling candidates computed + /// unconditionally, the exceptional cases masked in -- but with `Z2 = 1` the generic formula + /// simplifies: `U1 = X1`, `S1 = Y1` and `Z3 = H * Z1`, which drops five of its sixteen field + /// multiplications. The fixed-base comb multiplier adds a table entry to its accumulator on + /// every round, and every table entry is affine or the identity, so it is the caller this + /// exists for; `pub(crate)` because a caller has to know its point really has `Z = 1`. + pub(crate) fn add_affine( + &self, + x2: &P256K1FieldElement, + y2: &P256K1FieldElement, + other_is_infinity: Condition, + ) -> Self { + let self_is_infinity = self.is_infinity(); + + // The generic mixed addition, computed unconditionally: `U1 = X1`, `S1 = Y1`. + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2 + let r = self.y.sub(&s2); // S1 - S2 + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); // R^2 + H^3 - 2V + let y3 = acc.add(&v.sub(&x3).mul(&r)); // -S1*H^3 + (V - X3)*R + let z3 = h.mul(&self.z); // H * Z1 * Z2 with Z2 = 1 + let generic = Self { x: x3, y: y3, z: z3 }; + + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + let other = Self { x: *x2, y: *y2, z: P256K1FieldElement::ONE }; + + // Same priority order as `add`: the identity cases override everything else. + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, &other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for points that are **public**, branching on the exceptional cases instead + /// of computing every candidate and masking. + /// + /// [`Self::add`] must evaluate the doubling candidate on every call, because on secret inputs + /// it cannot branch on whether the doubling case applies -- that costs a full point doubling + /// (roughly a third of the addition) on every addition, whether or not it is ever used. This + /// version pays it only when the operands really are the same point, which for a scalar + /// multiplier over distinct precomputed multiples is essentially never. + /// + /// Restricted to `pub(crate)` and used only by [`crate::p256k1_wnaf`], whose scalars are + /// [`crate::p256k1_scalar::P256K1PublicScalar`] and whose points are a signer's public key and + /// the curve's own base point: every value it branches on is already known to an attacker. Do + /// not call this on anything derived from a private key or a per-message secret -- the + /// constant-time [`Self::add`] exists for that, and the scalar type split is what keeps the + /// two multipliers from being confused for one another. + pub(crate) fn add_vartime(&self, other: &Self) -> Self { + if self.is_infinity().to_bool() { + return *other; + } + if other.is_infinity().to_bool() { + return *self; + } + + let (generic, h, r) = self.generic_add(other); + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + generic + } + /// `self + (x2, y2)` for a **public**, affine `other` (`Z2 = 1`): [`Self::add_vartime`]'s + /// early-exit structure over [`Self::add_affine`]'s mixed formula, for the variable-time + /// multiplier's fixed side, whose precomputed multiples of `G` are stored affine. + /// `pub(crate)` for both of the reasons its two parents are. + pub(crate) fn add_vartime_affine( + &self, + x2: &P256K1FieldElement, + y2: &P256K1FieldElement, + ) -> Self { + if self.is_infinity().to_bool() { + return Self::from_affine(*x2, *y2); + } + + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2, with U1 = X1 + let r = self.y.sub(&s2); // S1 - S2, with S1 = Y1 + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); + let y3 = acc.add(&v.sub(&x3).mul(&r)); + let z3 = h.mul(&self.z); // H * Z1, with Z2 = 1 + Self { x: x3, y: y3, z: z3 } + } +} + +pub(crate) fn select_point( + cond: Condition, + a: &P256K1JacobianPoint, + b: &P256K1JacobianPoint, +) -> P256K1JacobianPoint { + P256K1JacobianPoint { + x: P256K1FieldElement::from_internal_limbs(select_limbs( + cond, + &a.x.internal_limbs(), + &b.x.internal_limbs(), + )), + y: P256K1FieldElement::from_internal_limbs(select_limbs( + cond, + &a.y.internal_limbs(), + &b.y.internal_limbs(), + )), + z: P256K1FieldElement::from_internal_limbs(select_limbs( + cond, + &a.z.internal_limbs(), + &b.z.internal_limbs(), + )), + } +} + +fn select_limbs(cond: Condition, a: &[u64; 4], b: &[u64; 4]) -> [u64; 4] { + let mut out = [0u64; 4]; + ct::conditional_select(cond, a, b, &mut out); + out +} + +// `add_vartime` is `pub(crate)` -- deliberately unreachable from outside the crate, since calling +// it on a secret point would undo the constant-time discipline [`P256K1JacobianPoint::add`] exists +// for -- so no integration test can reach it. Its whole contract is that it computes the same group +// law as `add`, differing only in *how* it gets there, so that is what is pinned here: agreement on +// every exceptional case, including the ones a scalar multiplier over distinct precomputed +// multiples essentially never reaches by chance (a point added to itself, and a point added to its +// own negation). QUALITY_AND_STYLE's private-function carve-out applies. +#[cfg(test)] +mod tests { + use super::*; + use crate::p256k1_domain::{G_X_LIMBS, G_Y_LIMBS}; + + #[test] + fn add_affine_agrees_with_add_on_every_case() { + let g = P256K1JacobianPoint::from_affine( + P256K1FieldElement::from_limbs(G_X_LIMBS), + P256K1FieldElement::from_limbs(G_Y_LIMBS), + ); + // A non-canonical-Z representation of G as well: `add_affine` reads `self.z`, so `self` + // must not be assumed affine even though `other` is. + let z = P256K1FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = P256K1JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + let selves = [g, g.double(), g.negate(), P256K1JacobianPoint::INFINITY, scaled_g]; + let others = [g, g.double(), g.negate()]; + for a in selves { + for b in others { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_affine(&bx, &by, Condition::::FALSE).to_affine(), + a.add(&b).to_affine(), + "add_affine disagrees with add" + ); + } + // The identity flag must win regardless of the coordinates passed alongside it. + assert_eq!( + a.add_affine(&g.x, &g.y, Condition::::TRUE).to_affine(), + a.to_affine(), + "add_affine with the identity flag must return self" + ); + } + } + + #[test] + fn add_vartime_affine_agrees_with_add_on_every_case() { + let g = P256K1JacobianPoint::from_affine( + P256K1FieldElement::from_limbs(G_X_LIMBS), + P256K1FieldElement::from_limbs(G_Y_LIMBS), + ); + let z = P256K1FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = P256K1JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + for a in [g, g.double(), g.negate(), P256K1JacobianPoint::INFINITY, scaled_g] { + for b in [g, g.double(), g.negate()] { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_vartime_affine(&bx, &by).to_affine(), + a.add(&b).to_affine(), + "add_vartime_affine disagrees with add" + ); + } + } + } + + #[test] + fn add_vartime_agrees_with_add_on_every_case() { + let g = P256K1JacobianPoint::from_affine( + P256K1FieldElement::from_limbs(G_X_LIMBS), + P256K1FieldElement::from_limbs(G_Y_LIMBS), + ); + // G, 2G, -G and the identity cover all four of `add`'s exceptional cases pairwise, plus + // the ordinary one (e.g. G + 2G). + let points = [g, g.double(), g.negate(), P256K1JacobianPoint::INFINITY]; + for a in points { + for b in points { + assert_eq!( + a.add_vartime(&b).to_affine(), + a.add(&b).to_affine(), + "add_vartime disagrees with add" + ); + } + } + } +} diff --git a/crypto/ec/src/p256k1_scalar.rs b/crypto/ec/src/p256k1_scalar.rs new file mode 100644 index 000000000..7902748ca --- /dev/null +++ b/crypto/ec/src/p256k1_scalar.rs @@ -0,0 +1,410 @@ +//! The secp256k1 scalar field, arithmetic mod the curve order `n` (SEC 2 v2 §2.4.1). Identical in +//! shape to [`crate::p256_scalar`] -- Montgomery form (`R = 2^256`), the same SOS reduction, the +//! same `Secret`/plain scalar split -- with `n` swapped for secp256k1's; see that module's docs for +//! the full derivation. +//! +//! REDC's accumulator, like [`crate::p256_scalar::redc`]'s, needs one bit of headroom beyond the +//! 8-limb product (`n` is close enough to `2^256` that `2n` can exceed it): verified (not checked +//! in) against 300,000 random trials plus the `(n-1)*(n-1)` worst case. + +use crate::nat; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +/// `n`, the order of the secp256k1 base point `G`, little-endian `u64` limbs (SEC 2 v2 §2.4.1). +pub const N_LIMBS: [u64; 4] = + [0xbfd25e8cd0364141, 0xbaaedce6af48a03b, 0xfffffffffffffffe, 0xffffffffffffffff]; + +/// `2^256 mod n`, also the Montgomery representation of `1`. +const R_LIMBS: [u64; 4] = + [0x402da1732fc9bebf, 0x4551231950b75fc4, 0x0000000000000001, 0x0000000000000000]; + +/// `R^2 mod n`, used to bring a plain value into Montgomery form. +const R_SQUARED_LIMBS: [u64; 4] = + [0x896cf21467d7d140, 0x741496c20e7cf878, 0xe697f5e45bcd07c6, 0x9d671cd581c69bc5]; + +/// `-n^-1 mod 2^64`, the Montgomery reduction constant. +const N_PRIME: u64 = 0x4b0dff665588b13f; + +/// `n - 2`, little-endian `u64` limbs -- the public exponent [`P256K1ScalarField::invert`] raises +/// its base to. +const N_MINUS_2_LIMBS: [u64; 4] = + [0xbfd25e8cd036413f, 0xbaaedce6af48a03b, 0xfffffffffffffffe, 0xffffffffffffffff]; + +/// An element of the secp256k1 scalar field, i.e. an integer mod `n`. Stored internally in +/// Montgomery form; see the module docs. +#[derive(Clone, Copy, Debug)] +pub struct P256K1ScalarField([u64; 4]); + +impl P256K1ScalarField { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0]); + + /// The multiplicative identity. Its Montgomery form is `R mod n`, not the literal integer `1`. + pub const ONE: Self = Self(R_LIMBS); + + /// Builds a scalar-field element from little-endian `u64` limbs (an ordinary, non-Montgomery + /// value), reducing once if `limbs >= n`, then converting to Montgomery form. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + Self(redc(&widening_mul(&reduced, &R_SQUARED_LIMBS))) + } + + /// Returns the canonical little-endian `u64` limbs (an ordinary, non-Montgomery value), `< n`. + pub fn to_limbs(&self) -> [u64; 4] { + let mut wide = [0u64; 8]; + wide[..4].copy_from_slice(&self.0); + redc(&wide) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + nat::is_zero(&self.0) + } + + /// `self + other mod n`. Operates directly on the Montgomery-form limbs: no REDC needed. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = nat::add(&self.0, &other.0); + let (sum_plus_r, _) = nat::add(&sum, &R_LIMBS); + let (diff, borrow) = nat::sub(&sum, &N_LIMBS); + let mut when_no_carry = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_r, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod n`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = nat::sub(&self.0, &other.0); + let (corrected, _) = nat::add(&diff, &N_LIMBS); + let mut result = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod n`. + pub fn negate(&self) -> Self { + let (diff, _) = nat::sub(&N_LIMBS, &self.0); + let mut result = [0u64; 4]; + ct::conditional_select(self.is_zero(), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod n`, via Montgomery multiplication. + pub fn mul(&self, other: &Self) -> Self { + Self(redc(&widening_mul(&self.0, &other.0))) + } + + /// Overwrites this value with zero, through a write the compiler may not elide. + /// + /// For the signing path, where a value of this type holds `d`, `k` or `k^-1` and would + /// otherwise stay legible on the stack after the signature is returned. This type is `Copy` + /// and carries public values too (verification's `u`, `v`, `e`, `r`, `s`), so it is not + /// wrapped in [`bouncycastle_utils::secret::Secret`] -- that would cost its `Copy`, its `const + /// ZERO`/`ONE`, and a scrub on every intermediate including the public ones. The trade is that + /// scrubbing is the caller's job, on every path out of the function; see + /// `bouncycastle_ecdsa`'s `sign_with_k` for the intended use. + /// + /// This does not reach values the compiler kept in registers or spilled itself, and is + /// defence in depth rather than a guarantee. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`widening_square`]. + /// + /// Used by the exponentiation loops ([`Self::invert`] and friends), which call it hundreds of + /// times in a row with nothing else competing for registers. Deliberately *not* used by this + /// curve's point arithmetic, which squares by calling `mul` with equal arguments: that was + /// tried and measured on P-256, and despite `square` being ~9% cheaper than `mul` in + /// isolation it made the constant-time comb multiplier consistently slower, because inlining + /// a third wide-multiply routine into that loop costs more than the saved partial products + /// return. See [`crate::p256::P256FieldElement::square`] for the numbers. + pub fn square(&self) -> Self { + Self(redc(&widening_square(&self.0))) + } + + /// Converts a secret scalar (`d` or `k`) into Montgomery form for field arithmetic. See + /// [`crate::p256_scalar::P256ScalarField::from_secret`]'s docs. + pub fn from_secret(secret: &P256K1Scalar) -> Self { + Self::from_limbs(*secret.limbs()) + } + + /// `self^-1 mod n`, or `0` if `self` is `0`. Fermat's little theorem (`n` is prime) over the + /// public exponent `n-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..4).rev() { + let limb = N_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } +} + +impl PartialEq for P256K1ScalarField { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. Comparing Montgomery-form limbs directly is valid: the map `x -> x*R mod n` is a + /// bijection. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..4 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for P256K1ScalarField {} + +/// Schoolbook widening multiply, identical in shape to [`crate::p256_scalar`]'s. +fn widening_mul(a: &[u64; 4], b: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + for i in 0..4 { + let mut carry: u128 = 0; + for j in 0..4 { + let idx = i + j; + let prod = (a[i] as u128) * (b[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + // Mutating `carry = s >> 64` here is an accepted mutant, not a bug -- see + // `crate::p521::widening_mul`'s identical tail loop for the general argument. + let mut k = i + 4; + while k < 8 { + let s = (result[k] as u128) + carry; + result[k] = s as u64; + carry = s >> 64; + k += 1; + } + } + result +} + +/// Schoolbook squaring of a 4-limb value into its 8-limb square. +/// +/// `a * a` is symmetric: the product `a_i * a_j` appears twice for every `i != j`. Forming each of +/// those once and doubling costs `L(L+1)/2` limb multiplications -- 10 at this width against +/// the 16 [`widening_mul`] would form for the same value. +/// +/// Three passes: the off-diagonal products `a_i * a_j` for `i < j`; a doubling of the whole +/// accumulator; then the diagonal squares `a_i * a_i` added in at limb `2i`. Writing `result[i + +/// 4]` in the first pass is an assignment rather than an accumulation because row `i` only ever +/// reaches limbs `2i + 1 ..= i + 4 - 1`, and no earlier row reaches limb `i + 4` either, so that +/// limb is still zero when the row's final carry lands on it. The doubling cannot overflow: the +/// off-diagonal sum is strictly less than `a^2 / 2`. +/// +/// Verified against Python's arbitrary-precision `**2` over 20,000 random 4-limb values plus the +/// all-ones worst case before being written here. +fn widening_square(a: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + + // Off-diagonal products, each formed once. + for i in 0..4 { + let mut carry: u128 = 0; + for j in (i + 1)..4 { + let idx = i + j; + let prod = (a[i] as u128) * (a[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + result[i + 4] = carry as u64; + } + + // Every off-diagonal product appears twice in the square. + let mut carry = 0u64; + for limb in result.iter_mut() { + let next_carry = *limb >> 63; + // `*limb << 1` always leaves bit 0 clear and `carry` is only ever 0 or 1, so the two + // operands are disjoint: mutating this `|` to `^` is an accepted equivalent, not a bug. + *limb = (*limb << 1) | carry; + carry = next_carry; + } + debug_assert_eq!(carry, 0, "doubling the off-diagonal sum overflowed 8 limbs"); + + // Diagonal squares. + let mut carry: u128 = 0; + for i in 0..4 { + let square = (a[i] as u128) * (a[i] as u128); + let low = (result[2 * i] as u128) + ((square as u64) as u128) + carry; + result[2 * i] = low as u64; + let high = (result[2 * i + 1] as u128) + (square >> 64) + (low >> 64); + result[2 * i + 1] = high as u64; + carry = high >> 64; + } + debug_assert_eq!(carry, 0, "adding the diagonal overflowed 8 limbs"); + + result +} + +/// `m * n`, one limb times the 4-limb modulus `n`, as the 5 limbs such a product can occupy. +/// +/// This is exactly what each round of [`redc`]'s SOS reduction needs, and the shape +/// [`crate::montgomery::redc`] has always used for the brainpool curves. Every hand-written scalar +/// REDC in this crate previously formed it with the general 4x4 [`widening_mul`] against a +/// zero-padded `[m, 0, ..., 0]` instead, asking for 16 limb multiplications where 4 are needed. +/// +/// Whether that padding costs anything turns out to depend entirely on the width, measured rather +/// than assumed: at four limbs the optimizer eliminates it completely (P-256 and secp256k1 time +/// identically either way), but it stops doing so by six, where the padded form measured 3.2x +/// slower per scalar multiplication for P-384 and 5.2x slower for P-521. All four are written this +/// way regardless, so the four modules read alike and none of them asks for work it does not want. +fn modulus_times_limb(m: u64) -> [u64; 5] { + let mut out = [0u64; 5]; + let mut carry: u128 = 0; + for j in 0..4 { + let prod = (m as u128) * (N_LIMBS[j] as u128) + carry; + out[j] = prod as u64; + carry = prod >> 64; + } + out[4] = carry as u64; + out +} + +/// Montgomery reduction (SOS method): given an 8-limb value `t < n*R`, returns `t * R^-1 mod n` as +/// 4 limbs. See [`crate::p256_scalar::redc`]'s docs for the algorithm and the 9-limb-accumulator +/// bound this mirrors (secp256k1's `n` is, like P-256's, close enough to `2^256` that `2n` can +/// exceed it, so the same one-extra-bit headroom is needed -- verified per the module docs, not +/// assumed). +fn redc(t: &[u64; 8]) -> [u64; 4] { + let mut acc = [0u64; 9]; + acc[..8].copy_from_slice(t); + + for i in 0..4 { + let m = acc[i].wrapping_mul(N_PRIME); + let mn = modulus_times_limb(m); + + let mut carry: u128 = 0; + for j in 0..5 { + let idx = i + j; + let s = (acc[idx] as u128) + (mn[j] as u128) + carry; + acc[idx] = s as u64; + carry = s >> 64; + } + // Propagate the carry through every limb above the window, up to the accumulator's + // proven 9-limb bound. The trip count depends only on the public round index `i`, + // never on the operands: once the carry has propagated out, the remaining iterations + // add 0. A `while carry != 0` here would be a secret-dependent branch on the signing + // path. + for idx in (i + 5)..9 { + let s = (acc[idx] as u128) + carry; + acc[idx] = s as u64; + carry = s >> 64; + } + debug_assert_eq!(carry, 0, "carry must be fully propagated within the 9-limb bound"); + } + debug_assert_eq!(&acc[..4], &[0u64; 4], "REDC's low limbs must be cleared after 4 rounds"); + debug_assert!(acc[8] == 0 || acc[8] == 1, "REDC's top limb must be a single bit"); + + let high: [u64; 4] = [acc[4], acc[5], acc[6], acc[7]]; + let (high_plus_r, _) = nat::add(&high, &R_LIMBS); + let (diff, borrow) = nat::sub(&high, &N_LIMBS); + let mut when_no_extra_bit = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(borrow), + &high, + &diff, + &mut when_no_extra_bit, + ); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(acc[8]), + &high_plus_r, + &when_no_extra_bit, + &mut result, + ); + result +} + +fn reduce_once(limbs: [u64; 4]) -> [u64; 4] { + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut result = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut result); + result +} + +/// A scalar mod `n`, `< n`, held in `Secret`. See [`crate::p256_scalar::P256Scalar`]'s docs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct P256K1Scalar(Secret<[u64; 4]>); + +impl P256K1Scalar { + /// Builds a secret scalar from little-endian `u64` limbs, reducing once if `limbs >= n`. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + let mut secret = Secret::<[u64; 4]>::new(); + *secret = reduce_once(limbs); + Self(secret) + } + + /// Builds a secret scalar from SEC 1 §2.3.7 big-endian octets, reducing once if `>= n`. + pub fn from_be_bytes(bytes: &[u8; 32]) -> Self { + Self::from_limbs(crate::p256k1_sec1::limbs_from_be_bytes(bytes)) + } + + /// Encodes to SEC 1 §2.3.7 big-endian octets. + pub fn to_be_bytes(&self) -> [u8; 32] { + crate::p256k1_sec1::be_bytes_from_limbs(&self.0) + } + + /// The scalar's limbs, for the crate's own multiplier and field-arithmetic implementations to + /// read. Not exposed outside the crate. + pub(crate) fn limbs(&self) -> &[u64; 4] { + &self.0 + } +} + +/// A scalar mod `n`, `< n`, held as a plain (non-secret) value. See +/// [`crate::p256_scalar::P256PublicScalar`]'s docs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct P256K1PublicScalar([u64; 4]); + +impl P256K1PublicScalar { + /// Builds a public scalar from little-endian `u64` limbs, reducing once if `limbs >= n`. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + Self(reduce_once(limbs)) + } + + /// Returns the scalar's little-endian `u64` limbs, `< n`. + pub fn to_limbs(&self) -> [u64; 4] { + self.0 + } + + /// `self^-1 mod n` in **variable time**, as a [`P256K1ScalarField`] ready for the arithmetic + /// that follows, or `0` for `0`. For ECDSA verification's `s` (FIPS 186-5 §6.4.2 step 4), + /// which is part of the signature and so already public; nothing derived from a private key + /// or a per-message secret may reach this, which is why it lives on the public scalar type + /// and not on [`P256K1ScalarField`], whose `invert` is the constant-time one. See + /// [`crate::inverse_vartime`]. + pub fn invert_vartime(&self) -> P256K1ScalarField { + P256K1ScalarField::from_limbs(crate::inverse_vartime::mod_inverse(&self.0, &N_LIMBS)) + } +} diff --git a/crypto/ec/src/p256k1_sec1.rs b/crypto/ec/src/p256k1_sec1.rs new file mode 100644 index 000000000..61274c221 --- /dev/null +++ b/crypto/ec/src/p256k1_sec1.rs @@ -0,0 +1,183 @@ +//! SEC 1 v2.0 (Certicom, May 2009) §2.3.3/§2.3.4 point encoding and decoding for secp256k1. Identical +//! in shape to [`crate::p256_sec1`] -- see that module's docs for the full reasoning (SEC 1 +//! §2.3.3/§2.3.4 cases, why there is no point-at-infinity case, why partial validation is full +//! validation here, and the compressed-decode square-root derivation) -- with the curve equation +//! `y^2 = x^3 + b` (`a = 0`, per [`crate::p256k1_point`], instead of `a = -3`) and secp256k1's +//! constants (also `p ≡ 3 (mod 4)` and cofactor `h = 1`, verified the same way in this module's +//! tests; SEC 2 v2 §2.4.1 gives both). + +use crate::nat; +use crate::p256k1::{P_LIMBS, P256K1FieldElement}; +use crate::p256k1_domain::B_LIMBS; +use crate::p256k1_point::P256K1JacobianPoint; + +/// `(p + 1) / 4`, the exponent [`decode`]'s compressed-point square root raises `α` to. +const SQRT_EXPONENT: [u64; 4] = + [0xffffffffbfffff0c, 0xffffffffffffffff, 0xffffffffffffffff, 0x3fffffffffffffff]; + +/// Converts 32 big-endian bytes to little-endian `u64` limbs. +pub fn limbs_from_be_bytes(bytes: &[u8; 32]) -> [u64; 4] { + [ + u64::from_be_bytes(bytes[24..32].try_into().unwrap()), + u64::from_be_bytes(bytes[16..24].try_into().unwrap()), + u64::from_be_bytes(bytes[8..16].try_into().unwrap()), + u64::from_be_bytes(bytes[0..8].try_into().unwrap()), + ] +} + +/// Converts little-endian `u64` limbs to 32 big-endian bytes. +pub fn be_bytes_from_limbs(limbs: &[u64; 4]) -> [u8; 32] { + let mut out = [0u8; 32]; + out[0..8].copy_from_slice(&limbs[3].to_be_bytes()); + out[8..16].copy_from_slice(&limbs[2].to_be_bytes()); + out[16..24].copy_from_slice(&limbs[1].to_be_bytes()); + out[24..32].copy_from_slice(&limbs[0].to_be_bytes()); + out +} + +fn limbs_less_than_p(limbs: &[u64; 4]) -> bool { + let (_, borrow) = nat::sub(limbs, &P_LIMBS); + borrow == 1 +} + +/// `true` iff either coordinate is outside `[0, p)` (SP 800-186 Appendix D.1.1.1 step 2). +fn either_out_of_range(x_limbs: &[u64; 4], y_limbs: &[u64; 4]) -> bool { + !limbs_less_than_p(x_limbs) || !limbs_less_than_p(y_limbs) +} + +/// `y^2 == x^3 + b`. +fn is_on_curve(x: &P256K1FieldElement, y: &P256K1FieldElement) -> bool { + let lhs = y.mul(y); + let x_cubed = x.mul(x).mul(x); + let rhs = x_cubed.add(&P256K1FieldElement::from_limbs(B_LIMBS)); + lhs == rhs +} + +/// `base^SQRT_EXPONENT mod p`. Plain (not masked) square-and-multiply: only ever runs on public +/// data (an incoming encoded point), like [`crate::p256_sec1::pow_public`]. +fn pow_public(base: &P256K1FieldElement, exponent_limbs: &[u64; 4]) -> P256K1FieldElement { + let mut result = P256K1FieldElement::ONE; + for limb_idx in (0..4).rev() { + let limb = exponent_limbs[limb_idx]; + for bit in (0..64).rev() { + result = result.square(); + if (limb >> bit) & 1 == 1 { + result = result.mul(base); + } + } + } + result +} + +/// SEC 1 §2.3.3 case 3: `04 || X || Y`, 65 bytes. +pub fn encode_uncompressed(x: &P256K1FieldElement, y: &P256K1FieldElement) -> [u8; 65] { + let mut out = [0u8; 65]; + out[0] = 0x04; + out[1..33].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out[33..65].copy_from_slice(&be_bytes_from_limbs(&y.to_limbs())); + out +} + +/// SEC 1 §2.3.3 case 2: `02 || X` or `03 || X`, 33 bytes, tag selected by `y mod 2`. +pub fn encode_compressed(x: &P256K1FieldElement, y: &P256K1FieldElement) -> [u8; 33] { + let mut out = [0u8; 33]; + out[0] = if y.to_limbs()[0] & 1 == 0 { 0x02 } else { 0x03 }; + out[1..33].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out +} + +/// Decodes and validates a SEC 1 §2.3.4 point encoding: 33 bytes (compressed) or 65 bytes +/// (uncompressed). See [`crate::p256_sec1::decode`]'s docs for exactly which checks this performs. +pub fn decode(bytes: &[u8]) -> Option<(P256K1FieldElement, P256K1FieldElement)> { + match bytes.len() { + 33 => { + let y_tilde: u64 = match bytes[0] { + 0x02 => 0, + 0x03 => 1, + _ => return None, + }; + let x_bytes: [u8; 32] = bytes[1..33].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + if !limbs_less_than_p(&x_limbs) { + return None; + } + let x = P256K1FieldElement::from_limbs(x_limbs); + let x_cubed = x.mul(&x).mul(&x); + let alpha = x_cubed.add(&P256K1FieldElement::from_limbs(B_LIMBS)); + let beta = pow_public(&alpha, &SQRT_EXPONENT); + if beta.mul(&beta) != alpha { + return None; + } + let beta_parity = beta.to_limbs()[0] & 1; + let y = if beta_parity == y_tilde { beta } else { beta.negate() }; + Some((x, y)) + } + 65 => { + if bytes[0] != 0x04 { + return None; + } + let x_bytes: [u8; 32] = bytes[1..33].try_into().ok()?; + let y_bytes: [u8; 32] = bytes[33..65].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + let y_limbs = limbs_from_be_bytes(&y_bytes); + if either_out_of_range(&x_limbs, &y_limbs) { + return None; + } + let x = P256K1FieldElement::from_limbs(x_limbs); + let y = P256K1FieldElement::from_limbs(y_limbs); + if !is_on_curve(&x, &y) { + return None; + } + Some((x, y)) + } + _ => None, + } +} + +/// Decodes and validates an encoded public key (see [`decode`]) directly into a +/// [`P256K1JacobianPoint`]. +pub fn decode_point(bytes: &[u8]) -> Option { + let (x, y) = decode(bytes)?; + Some(P256K1JacobianPoint::from_affine(x, y)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn p_is_3_mod_4() { + assert_eq!(P_LIMBS[0] & 0b11, 3); + } + + #[test] + fn limbs_less_than_p_boundary_cases() { + assert!(limbs_less_than_p(&[0, 0, 0, 0])); + + let mut p_minus_1 = P_LIMBS; + p_minus_1[0] -= 1; + assert!(limbs_less_than_p(&p_minus_1)); + + assert!(!limbs_less_than_p(&P_LIMBS)); + + // `P_LIMBS`' top three limbs are already all-`1`s, so (unlike P-256/P-384/P-521, which + // have headroom in a non-adjacent limb) the only representable value strictly above `p` + // is `p + 1` itself. + let mut p_plus_1 = P_LIMBS; + p_plus_1[0] += 1; + assert!(!limbs_less_than_p(&p_plus_1)); + + assert!(!limbs_less_than_p(&[u64::MAX; 4])); + } + + #[test] + fn either_out_of_range_requires_only_one_coordinate_to_be_out_of_range() { + let in_range = [0u64; 4]; + let out_of_range = P_LIMBS; + + assert!(!either_out_of_range(&in_range, &in_range)); + assert!(either_out_of_range(&out_of_range, &in_range)); + assert!(either_out_of_range(&in_range, &out_of_range)); + assert!(either_out_of_range(&out_of_range, &out_of_range)); + } +} diff --git a/crypto/ec/src/p256k1_wnaf.rs b/crypto/ec/src/p256k1_wnaf.rs new file mode 100644 index 000000000..a7276b1c0 --- /dev/null +++ b/crypto/ec/src/p256k1_wnaf.rs @@ -0,0 +1,142 @@ +//! Interleaved windowed-NAF (wNAF) Shamir's-trick multiplication: `[u]G + [v]Q`, for secp256k1 +//! ECDSA verification. Identical algorithm to [`crate::p256_wnaf`] -- see that module's docs for +//! the full derivation and verification methodology -- with the limb count swapped for +//! secp256k1's (`WNAF_LEN = 257`, one more than `n`'s 256-bit length, same margin as P-256's own +//! `257 = 256 + 1`). +//! +//! `G`'s odd multiples are a compile-time affine table (`p256k1_wnaf_table`, width 7) added by mixed +//! addition; only `Q`'s table is built per call. See [`crate::p256_wnaf`]. + +use crate::nat; +use crate::p256k1::P256K1FieldElement; +use crate::p256k1_point::P256K1JacobianPoint; +use crate::p256k1_scalar::P256K1PublicScalar; +use crate::p256k1_wnaf_table::{G_ODD_MULTIPLES_X, G_ODD_MULTIPLES_Y}; + +/// Window for `Q`, whose odd multiples are computed per call: 8 point additions to build the +/// table, one addition per ~6 digits to use it. +const WIDTH: usize = 5; +/// Window for `G`, whose odd multiples are a compile-time table +/// ([`G_ODD_MULTIPLES_X`]/[`G_ODD_MULTIPLES_Y`]): nothing to build, so the widest window a +/// signed `i8` digit allows, giving one addition per ~8 digits instead of ~6, and every one of +/// them a mixed addition against an affine entry. +const G_WIDTH: usize = 7; +const WNAF_LEN: usize = 257; +// Mutating this expression (e.g. `WIDTH - 2` -> `+ 2`) only grows the table `odd_multiples` builds +// and `lookup_signed` reads from; the actual digit range `compute_wnaf` can produce is bounded by +// `WIDTH` itself (unchanged), so the extra entries are simply never indexed -- an accepted mutant. +const ODD_MULTIPLE_COUNT: usize = 1 << (WIDTH - 2); + +/// `[u]G + [v]Q`. +pub fn shamir_multiply( + u: &P256K1PublicScalar, + v: &P256K1PublicScalar, + q: &P256K1JacobianPoint, +) -> P256K1JacobianPoint { + let du = compute_wnaf(u.to_limbs(), G_WIDTH); + let dv = compute_wnaf(v.to_limbs(), WIDTH); + + let table_q = odd_multiples(q); + + let mut r = P256K1JacobianPoint::INFINITY; + for i in (0..WNAF_LEN).rev() { + r = r.double(); + if let Some((x, y)) = lookup_signed_g(du[i]) { + r = r.add_vartime_affine(&x, &y); + } + if let Some(add_q) = lookup_signed(&table_q, dv[i]) { + r = r.add_vartime(&add_q); + } + } + r +} + +fn shr1(limbs: &[u64; 4]) -> [u64; 4] { + [ + (limbs[0] >> 1) | ((limbs[1] & 1) << 63), + (limbs[1] >> 1) | ((limbs[2] & 1) << 63), + (limbs[2] >> 1) | ((limbs[3] & 1) << 63), + limbs[3] >> 1, + ] +} + +/// The width-`width` wNAF digits of `k` (`width` at most 7, so a digit fits an `i8`), LSB-first; unused trailing entries are `0`. Not +/// constant time: only ever called on public scalars (see the module docs). +fn compute_wnaf(k_limbs: [u64; 4], width: usize) -> [i8; WNAF_LEN] { + let mut digits = [0i8; WNAF_LEN]; + let mut k = k_limbs; + let mut pos = 0; + while k != [0, 0, 0, 0] { + if k[0] & 1 == 1 { + // Mutating this mask (e.g. `(1 << WIDTH) - 1` -> `+ 1`) is an accepted mutant, not a + // bug: whatever mask is used, `digit` always keeps `k[0]`'s low (odd) bit, so it's + // always odd -- the only invariant `lookup_signed` and the subtract-or-add-then-`shr1` + // step below actually rely on. A degenerate mask just makes this compute a + // non-minimal (but still exactly reconstructing `k`) signed-digit sequence -- see + // `crate::p521_wnaf`'s identical case for the full argument and verification. + let mut digit = (k[0] & ((1 << width) - 1)) as i16; + if digit >= 1 << (width - 1) { + digit -= 1 << width; + } + k = if digit >= 0 { + let (new_k, borrow) = nat::sub(&k, &[digit as u64, 0, 0, 0]); + debug_assert_eq!(borrow, 0); + new_k + } else { + let (new_k, carry) = nat::add(&k, &[(-digit) as u64, 0, 0, 0]); + debug_assert_eq!(carry, 0); + new_k + }; + debug_assert!(pos < WNAF_LEN, "wNAF encoding overflowed its verified bound"); + digits[pos] = digit as i8; + } + k = shr1(&k); + pos += 1; + } + digits +} + +/// The odd multiples `1*p, 3*p, 5*p, ..., (2*ODD_MULTIPLE_COUNT - 1)*p`, indexed so entry `i` +/// holds `(2i+1)*p`. +fn odd_multiples(p: &P256K1JacobianPoint) -> [P256K1JacobianPoint; ODD_MULTIPLE_COUNT] { + let double_p = p.double(); + let mut table = [*p; ODD_MULTIPLE_COUNT]; + let mut cur = *p; + for entry in table.iter_mut() { + *entry = cur; + cur = cur.add_vartime(&double_p); + } + table +} + +/// `digit * p` where `p` is the point `table` was built from ([`odd_multiples`]), or `None` for +/// digit `0`. +fn lookup_signed( + table: &[P256K1JacobianPoint; ODD_MULTIPLE_COUNT], + digit: i8, +) -> Option { + // Every accepted-mutant survivor below relies on `digit` always being odd (it's built from an + // odd `k[0]` in `compute_wnaf`, see that function's docs): `digit > 0` vs `>= 0` agree once + // the `digit == 0` case is already handled above, and for odd `digit`, `(digit - 1) / 2 == + // digit / 2` (and symmetrically for `-digit`), since integer division already floors an odd + // numerator down to the same value subtracting 1 first would. + if digit == 0 { + None + } else if digit > 0 { + Some(table[(digit as usize - 1) / 2]) + } else { + Some(table[(-digit as usize - 1) / 2].negate()) + } +} + +/// `digit * G` for a width-[`G_WIDTH`] digit, read from the compile-time table of odd multiples +/// of `G` as affine coordinates (negating `y` for a negative digit), or `None` for digit `0`. +fn lookup_signed_g(digit: i8) -> Option<(P256K1FieldElement, P256K1FieldElement)> { + if digit == 0 { + return None; + } + let index = (digit.unsigned_abs() as usize - 1) / 2; + let x = P256K1FieldElement::from_limbs(G_ODD_MULTIPLES_X[index]); + let y = P256K1FieldElement::from_limbs(G_ODD_MULTIPLES_Y[index]); + Some(if digit > 0 { (x, y) } else { (x, y.negate()) }) +} diff --git a/crypto/ec/src/p256k1_wnaf_table.rs b/crypto/ec/src/p256k1_wnaf_table.rs new file mode 100644 index 000000000..3df038c91 --- /dev/null +++ b/crypto/ec/src/p256k1_wnaf_table.rs @@ -0,0 +1,105 @@ +//! The checked-in odd multiples of the base point `G` for [`crate::p256k1_wnaf`]'s fixed side: +//! `G_ODD_MULTIPLES_X[i]`/`G_ODD_MULTIPLES_Y[i]` are the affine coordinates of `(2i + 1) G`, for +//! `i` in `0..32`, as little-endian `u64` limbs -- the table a width-7 wNAF over `G` indexes. +//! `Q`'s table is built at run time because `Q` is the signer's key; `G` is the curve's constant, +//! so its table need not be. +//! +//! Generated in Python from the affine group law (SP 800-186 Appendix A.1.1) and SEC 2 v2 §2.4.1's `G`, +//! independently of this crate's arithmetic, and checked there by `n * G == infinity`; this +//! module's own test regenerates every entry with the crate's point arithmetic and compares. + +/// `x` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_X: [[u64; 4]; 32] = [ + [0x59f2815b16f81798, 0x029bfcdb2dce28d9, 0x55a06295ce870b07, 0x79be667ef9dcbbac], + [0x8601f113bce036f9, 0xb531c845836f99b0, 0x49344f85f89d5229, 0xf9308a019258c310], + [0xcba8d569b240efe4, 0xe88b84bddc619ab7, 0x55b4a7250a5c5128, 0x2f8bde4d1a072093], + [0xe92bddedcac4f9bc, 0x3d419b7e0330e39c, 0xa398f365f2ea7a0e, 0x5cbdf0646e5db4ea], + [0xc35f110dfc27ccbe, 0xe09796974c57e714, 0x09ad178a9f559abd, 0xacd484e2f0c7f653], + [0xbbec17895da008cb, 0x5649980be5c17891, 0x5ef4246b70c65aac, 0x774ae7f858a9411e], + [0xdeeddf8f19405aa8, 0xb075fbc6610e58cd, 0xc7d1d205c3748651, 0xf28773c2d975288b], + [0x44adbcf8e27e080e, 0x31e5946f3c85f79e, 0x5a465ae3095ff411, 0xd7924d4f7d43ea96], + [0x66e4faa04a2d4a34, 0xeb9898ae79b97687, 0xa420fee807eacf21, 0xdefdea4cdb677750], + [0x7475656138385b6c, 0xf06acfebd7e86d27, 0x93ef5cff444f4979, 0x2b4ea0a797a443d2], + [0x81340aef25be59d5, 0x1d9ad40271f81071, 0x4f93fa332ce33330, 0x352bbf4a4cdd1256], + [0xdc9cdadd4ecacc3f, 0xe42ab8dfeff5ff29, 0x0230010559879124, 0x2fa2104d6b38d11b], + [0x69ca0cd7f5453714, 0x263c3d84e09572e2, 0xab21a9b066edda83, 0x9248279b09b4d68d], + [0x7e996d443dee8729, 0x2f570e144bf615c0, 0x8e70132fb0beb752, 0xdaed4f2be3a8bf27], + [0xe6a3b5e87d22e7db, 0x11ecd9e9fdf281b0, 0x8acf28d7cbb19f90, 0xc44d12c7065d812e], + [0xb61c65cbd269e6b4, 0x152b695336c28063, 0xc89a20cfded60853, 0x6a245bf6dc698504], + [0xf95ae57f0d0bd6a5, 0xce13300b0bec1146, 0xc077e3d2fe541084, 0x1697ffa6fd9de627], + [0xf982345ef27a7479, 0x9deb8360ffb7f61d, 0x986d0f07e834cb0d, 0x605bdb019981718b], + [0xfe31c7e9d87ff33d, 0xdcb01c354959b10c, 0x7402fdc45a215e10, 0x62d14dab4150bf49], + [0x5e555c2f86308b6f, 0x2c50e9f56b9b8b42, 0xde5b4b06c408e56b, 0x80c60ad0040f27da], + [0x9d5eabb0fa03c8fb, 0x4cc5dc9487d84704, 0xaa74c6348cc54d34, 0x7a9375ad6167ad54], + [0x4bb51f459bc3ffc9, 0xbb408ec39b68df50, 0x907a9ed045447a79, 0xd528ecd9b696b54c], + [0x87231808f8b45963, 0x5266115e4a7ecb13, 0xea25f514e8ecdad0, 0x049370a4b5f43412], + [0xf1c13eb1fc345d74, 0x881d811e0e1498e2, 0xd73df930d64702ef, 0x77f230936ee88cbb], + [0xeb28531b7739f530, 0x58c80074ab9d4dba, 0xea44887e5c7c0bce, 0xf2dac991cc4ce4b9], + [0xbcba4850c690d45b, 0x5a216cdfc9dae3de, 0x1b4be8fbbe252012, 0x463b3d9f662621fb], + [0xa32496b49998f247, 0x6b98fac14328a2d1, 0x09232d4aff3b5997, 0xf16f804244e46e2a], + [0x369e15f7151d41d1, 0x5d245315ace27c65, 0xb0352b7a14311af5, 0xcaf754272dc84563], + [0x24497bc86f082120, 0x44a09c07cb86d7c1, 0xf85d0f1709979d8b, 0x2600ca4b282cb986], + [0xc602a7746998e435, 0x01c48685e24f7dc8, 0x338ec53cd12220bc, 0x7635ca72d7e8432c], + [0xc1a50743bf56cc18, 0xb7f2b33479d468fb, 0xdbbf4a87deee8a66, 0x754e3239f325570c], + [0x9fe2694691d9b9e8, 0x330800661d1c952f, 0xff57859c82d570f0, 0xe3e6bd1071a1e96a], +]; + +/// `y` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_Y: [[u64; 4]; 32] = [ + [0x9c47d08ffb10d4b8, 0xfd17b448a6855419, 0x5da4fbfc0e1108a8, 0x483ada7726a3c465], + [0x6cb9fd7584b8e672, 0x6500a99934c2231b, 0x0fe337e62a37f356, 0x388f7b0f632de814], + [0xdca87d3aa6ac62d6, 0xf788271bab0d6840, 0xd4dba9dda6c9c426, 0xd8ac222636e5e3d6], + [0xa5082628087264da, 0xa813d0b813fde7b5, 0xa3178d6d861a54db, 0x6aebca40ba255960], + [0x05cc262ac64f9c37, 0xadd888a4375f8e0f, 0x64380971763b61e9, 0xcc338921b0a7d9fd], + [0x301d74c9c953c61b, 0x372db1e2dff9d6a8, 0x0243dd56d7b7b365, 0xd984a032eb6b5e19], + [0x29b5cb52db03ed81, 0x3a1a06da521fa91f, 0x758212eb65cdaf47, 0x0ab0902e8d880a89], + [0xc504dc9ff6a26b58, 0xea40af2bd896d3a5, 0x83842ec228cc6def, 0x581e2872a86c72a6], + [0xcfb199f69e56eb77, 0xced1f4a04a95c0f6, 0xe997b0ead2a93dae, 0x4211ab0694635168], + [0xb570c854e5c09b7a, 0x1a01f60c50269763, 0xb343083b5a1c8613, 0x85e89bc037945d93], + [0x67bd3d8bcf81998c, 0x4a1b3b2e71b1039c, 0xd59c18259dda3e1f, 0x321eb4075348f534], + [0x423ba76b532b7d67, 0x181d70ecfc882648, 0xb64569335bd5dd80, 0x02de1068295dd865], + [0xe54a32ce97cb3402, 0x3fc0de2a887912ff, 0x5d1aa71bdea2b1ff, 0x73016f7bf234aade], + [0xab40e52290be1c55, 0x3f83c230f3afa726, 0xd4a1aca87ef8d700, 0xa69dce4a7d6c98e8], + [0xa039063f0e0e6482, 0x0e106e861edf61c5, 0x76c45926c982fdac, 0x2119a460ce326cdc], + [0xfd5e6348100d8a82, 0x8b33ba48d0423b6e, 0x8b3f5126f16a24ad, 0xe022cf42c2bd4a70], + [0xadee9d63d01b2396, 0xa2cf15009e498ae7, 0x27561506e4557433, 0xb9c398f186806f5d], + [0x3b01e1e9056b8c49, 0xc26bfae84fb14db4, 0x81a78d93ec96fe23, 0x02972d2de4f8d206], + [0x35f5642483b25eaf, 0x01aa132967ab4722, 0x98088a1950eed0db, 0x80fc06bd8cc5b010], + [0x1aa01f56430bd57a, 0xa65eed4cbe7024eb, 0x26e66bad7fe72f70, 0x1c38303f1cc5c30f], + [0x02d499ec224dc7f7, 0xbdc59ea10c70ce2b, 0x09559e0d79269046, 0x0d0e3fa9eca87269], + [0x063465b521409933, 0xbc4345405c520dbc, 0x9966f21881fd656e, 0xeecf41253136e5f9], + [0xb653052a12949c9a, 0x54c3f3afbb5b6764, 0x8b3081b0512fd62a, 0x758f3f41afd6ed42], + [0xbe8eb3c7671c60d6, 0x96c95330d97077cb, 0x0a08266e9ba1b378, 0x958ef42a7886b640], + [0x1a117dba703a3c37, 0x9eb5fbeb0598e4fd, 0x4da1f32dec2531df, 0xe0dedc9b3b2f8dad], + [0x1cb377b01af7307e, 0xc622e27c970a1de3, 0x43114306dd8622d7, 0x5ed430d78c296c35], + [0xd6579962c4e31df6, 0x2a6c53c26e5cce26, 0x13d206fcdf4e33d9, 0xcedabd9b82203f7e], + [0xc32f908318a04476, 0x5f4fa9b7962232a5, 0xa41b643fa5e46057, 0xcb474660ef35f5f2], + [0x4b0be9475a7e4b40, 0x5ac6be74ab5f0ef4, 0xa693b03fcddbb45d, 0x4119b88753c15bd6], + [0xd9e76f302c5b9c61, 0x4ecfc061d57048ba, 0x3d1d5e590f78e6d7, 0x091b649609489d61], + [0x0c5d98093c536683, 0x23ee33d0197a695d, 0xb3cd0ed304ea49a0, 0x0673fb86e5bda30f], + [0x67002af4920e37f5, 0xa5a2283993e90c41, 0x40c0aa58379a3cb6, 0x59c9e0bba394e76f], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::p256k1::P256K1FieldElement; + use crate::p256k1_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::p256k1_point::P256K1JacobianPoint; + + #[test] + fn table_matches_the_odd_multiples_of_g_computed_by_the_point_arithmetic() { + let g = P256K1JacobianPoint::from_affine( + P256K1FieldElement::from_limbs(G_X_LIMBS), + P256K1FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + let mut cur = g; + for i in 0..32 { + let (x, y) = cur.to_affine().expect("an odd multiple of G below n is never infinity"); + assert_eq!(x.to_limbs(), G_ODD_MULTIPLES_X[i], "entry {i}: x of {}G", 2 * i + 1); + assert_eq!(y.to_limbs(), G_ODD_MULTIPLES_Y[i], "entry {i}: y of {}G", 2 * i + 1); + cur = cur.add_vartime(&two_g); + } + } +} diff --git a/crypto/ec/src/p384.rs b/crypto/ec/src/p384.rs new file mode 100644 index 000000000..c45bb6fdd --- /dev/null +++ b/crypto/ec/src/p384.rs @@ -0,0 +1,395 @@ +//! The P-384 base field, GF(p) for `p = 2^384 - 2^128 - 2^96 + 2^32 - 1`. +//! +//! Domain parameters are from NIST SP 800-186 (Feb 2023) §3.2.1.4, "Curve P-384": the prime `p` +//! quoted there in hex is +//! `0xffffffff ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe ffffffff 00000000 +//! 00000000 ffffffff` (384 bits, most significant word first). [`P_LIMBS`] below is that same +//! value in little-endian `u64` limbs. +//! +//! # Reduction algorithm +//! +//! Same shape as [`crate::p256`]'s (see that module's docs): `p` is a generalized Mersenne number, +//! so a 768-bit product reduces to a fixed sum and difference of 384-bit terms assembled from the +//! product's own 32-bit words, with no multiplication at all. SP 800-186 (Feb 2023) Appendix +//! G.1.3, "Curve P-384", gives the expression -- `B = (T + 2S1 + S2 + S3 + S4 + S5 + S6 - D1 - D2 +//! - D3) mod p` -- and [`reduce`] transcribes it directly, so a reviewer with G.1.3 open can match +//! its ten terms to the document row by row. +//! +//! G.1's preamble states the precondition ("given an integer A less than m^2") and the shape of +//! the leftover work ("the integer sum or difference can be evaluated and the result reduced +//! modulo m. The latter reduction can be accomplished by adding or subtracting a few copies of +//! m."); [`reduce`] carries the exact bound on how many copies that is here. +//! +//! The identity `2^384 = p + C` with `C = 2^128 + 2^96 - 2^32 + 1` (i.e. `2^384 mod p = C`) +//! survives in two places: [`P384FieldElement::add`] uses it to correct a carry out of the top +//! limb, and [`reduce`]'s final fold uses it on the accumulator's top limb. + +use crate::nat; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// `p`, little-endian `u64` limbs (SP 800-186 §3.2.1.4). +pub const P_LIMBS: [u64; 6] = [ + 0x00000000ffffffff, 0xffffffff00000000, 0xfffffffffffffffe, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, +]; + +/// `2^384 mod p = 2^128 + 2^96 - 2^32 + 1`, little-endian `u64` limbs. This is the constant +/// [`reduce`]'s fold uses in place of `2^384`. +const C_LIMBS: [u64; 6] = [0xffffffff00000001, 0x00000000ffffffff, 0x0000000000000001, 0, 0, 0]; + +/// `p - 2`, little-endian `u64` limbs -- the public exponent [`P384FieldElement::invert`] raises +/// its base to, per Fermat's little theorem. +const P_MINUS_2_LIMBS: [u64; 6] = [ + 0x00000000fffffffd, 0xffffffff00000000, 0xfffffffffffffffe, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, +]; + +/// `4p`, little-endian `u64` limbs -- the bias [`reduce`] starts its accumulator at so that +/// subtracting SP 800-186 §G.1.3's three `D` terms can never take it below zero. See [`reduce`] +/// for why `4` is the right multiple. +const FOUR_P_LIMBS: [u64; 7] = [ + 0x00000003fffffffc, 0xfffffffc00000000, 0xfffffffffffffffb, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0x0000000000000003, +]; + +/// An element of the P-384 base field GF(p), always held in canonical reduced form (`< p`). +#[derive(Clone, Copy, Debug)] +pub struct P384FieldElement([u64; 6]); + +impl P384FieldElement { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0, 0, 0]); + + /// The multiplicative identity. + pub const ONE: Self = Self([1, 0, 0, 0, 0, 0]); + + /// Builds a field element from little-endian `u64` limbs, reducing once if `limbs >= p`. Any + /// `u64` limb pattern is accepted: since `limbs < 2^384 < 2*p`, a single conditional + /// subtraction of `p` always suffices to bring it into `[0, p)`. + pub fn from_limbs(limbs: [u64; 6]) -> Self { + let (diff, borrow) = nat::sub(&limbs, &P_LIMBS); + let mut result = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut result); + Self(result) + } + + /// Returns the canonical little-endian `u64` limbs, `< p`. + pub fn to_limbs(&self) -> [u64; 6] { + self.0 + } + + /// The limbs exactly as this type stores them: the canonical value itself, so this is + /// [`Self::to_limbs`] by another name. Paired with [`Self::from_internal_limbs`] so the + /// crate's point arithmetic can select between the coordinates of two existing points without + /// re-reducing each one. + pub(crate) fn internal_limbs(&self) -> [u64; 6] { + self.0 + } + + /// Wraps limbs already in this type's stored representation (see [`Self::internal_limbs`]), + /// skipping the reduction and conversion [`Self::from_limbs`] performs. Only for values that + /// came out of another element of this type, or a constant computed in that representation: + /// the masked selects in the point arithmetic, and the comb table's infinity sentinel. Not + /// exposed, since an out-of-range value would break every equality and reduction assumption + /// downstream; the range is checked in debug builds. + pub(crate) fn from_internal_limbs(limbs: [u64; 6]) -> Self { + debug_assert_eq!( + crate::nat::sub(&limbs, &P_LIMBS).1, + 1, + "from_internal_limbs given a value >= p" + ); + Self(limbs) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + nat::is_zero(&self.0) + } + + /// Overwrites the element with zero, for scrubbing a secret-derived value before it goes out + /// of scope. Defence in depth rather than a guarantee: copies the compiler kept in registers + /// or spilled itself are not reached. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self + other mod p`. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = nat::add(&self.0, &other.0); + let (sum_plus_c, _) = nat::add(&sum, &C_LIMBS); + let (diff, borrow) = nat::sub(&sum, &P_LIMBS); + let mut when_no_carry = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 6]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_c, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod p`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = nat::sub(&self.0, &other.0); + let (corrected, _) = nat::add(&diff, &P_LIMBS); + let mut result = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod p`. + pub fn negate(&self) -> Self { + let (diff, _) = nat::sub(&P_LIMBS, &self.0); + let mut result = [0u64; 6]; + ct::conditional_select(nat::is_zero(&self.0), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod p`. + pub fn mul(&self, other: &Self) -> Self { + Self(reduce(&widening_mul(&self.0, &other.0))) + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`widening_square`]. + /// + /// Used by the exponentiation loops ([`Self::invert`] and friends), which call it hundreds of + /// times in a row with nothing else competing for registers. Deliberately *not* used by this + /// curve's point arithmetic, which squares by calling `mul` with equal arguments: that was + /// tried and measured on P-256, and despite `square` being ~9% cheaper than `mul` in + /// isolation it made the constant-time comb multiplier consistently slower, because inlining + /// a third wide-multiply routine into that loop costs more than the saved partial products + /// return. See [`crate::p256::P256FieldElement::square`] for the numbers. + pub fn square(&self) -> Self { + Self(reduce(&widening_square(&self.0))) + } + + /// `self^-1 mod p`, or `0` if `self` is `0`. Fermat's little theorem over the public + /// exponent `p-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..6).rev() { + let limb = P_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } +} + +impl PartialEq for P384FieldElement { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..6 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for P384FieldElement {} + +/// Schoolbook widening multiply of two 6-limb (384-bit) operands into a 12-limb (768-bit) product. +/// See [`crate::p256`]'s widening_mul docs for why this is duplicated per curve. +fn widening_mul(a: &[u64; 6], b: &[u64; 6]) -> [u64; 12] { + let mut result = [0u64; 12]; + for i in 0..6 { + let mut carry: u128 = 0; + for j in 0..6 { + let idx = i + j; + let prod = (a[i] as u128) * (b[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + let mut k = i + 6; + while k < 12 { + let s = (result[k] as u128) + carry; + result[k] = s as u64; + carry = s >> 64; + k += 1; + } + } + result +} + +/// The 32-bit word `A_i` of the 768-bit product, in SP 800-186 §G.1.3's numbering: that appendix +/// writes the product as `A = (A23 || A22 || ... || A0)` with each `A_i` a 32-bit integer, `A0` +/// least significant. +fn word(t: &[u64; 12], i: usize) -> u64 { + (t[i / 2] >> (32 * (i % 2))) & 0xffff_ffff +} + +/// Assembles one of §G.1.3's 384-bit terms from its twelve 32-bit words. The document prints each +/// term most significant word first; this takes them least significant first, so each call site +/// below reads its document row right to left. +fn term(w: [u64; 12]) -> [u64; 6] { + [ + w[0] | (w[1] << 32), + w[2] | (w[3] << 32), + w[4] | (w[5] << 32), + w[6] | (w[7] << 32), + w[8] | (w[9] << 32), + w[10] | (w[11] << 32), + ] +} + +/// Zero-extends a 6-limb value to 7 limbs, for arithmetic against the 7-limb accumulator. +fn widen(v: &[u64; 6]) -> [u64; 7] { + [v[0], v[1], v[2], v[3], v[4], v[5], 0] +} + +/// `small * v`, one limb times a 6-limb value, as 7 limbs. Only ever called with a `small` that is +/// a bounded, operand-independent count (see [`reduce`]), never a secret. +fn mul_small(small: u64, v: &[u64; 6]) -> [u64; 7] { + let mut out = [0u64; 7]; + let mut carry: u128 = 0; + for i in 0..6 { + let prod = (small as u128) * (v[i] as u128) + carry; + out[i] = prod as u64; + carry = prod >> 64; + } + out[6] = carry as u64; + out +} + +/// Schoolbook squaring of a 6-limb value into its 12-limb square. +/// +/// `a * a` is symmetric: the product `a_i * a_j` appears twice for every `i != j`. Forming each of +/// those once and doubling costs `L(L+1)/2` limb multiplications -- 21 at this width against +/// the 36 [`widening_mul`] would form for the same value. +/// +/// Three passes: the off-diagonal products `a_i * a_j` for `i < j`; a doubling of the whole +/// accumulator; then the diagonal squares `a_i * a_i` added in at limb `2i`. Writing `result[i + +/// 6]` in the first pass is an assignment rather than an accumulation because row `i` only ever +/// reaches limbs `2i + 1 ..= i + 6 - 1`, and no earlier row reaches limb `i + 6` either, so that +/// limb is still zero when the row's final carry lands on it. The doubling cannot overflow: the +/// off-diagonal sum is strictly less than `a^2 / 2`. +/// +/// Verified against Python's arbitrary-precision `**2` over 20,000 random 6-limb values plus the +/// all-ones worst case before being written here. +fn widening_square(a: &[u64; 6]) -> [u64; 12] { + let mut result = [0u64; 12]; + + // Off-diagonal products, each formed once. + for i in 0..6 { + let mut carry: u128 = 0; + for j in (i + 1)..6 { + let idx = i + j; + let prod = (a[i] as u128) * (a[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + result[i + 6] = carry as u64; + } + + // Every off-diagonal product appears twice in the square. + let mut carry = 0u64; + for limb in result.iter_mut() { + let next_carry = *limb >> 63; + // `*limb << 1` always leaves bit 0 clear and `carry` is only ever 0 or 1, so the two + // operands are disjoint: mutating this `|` to `^` is an accepted equivalent, not a bug. + *limb = (*limb << 1) | carry; + carry = next_carry; + } + debug_assert_eq!(carry, 0, "doubling the off-diagonal sum overflowed 12 limbs"); + + // Diagonal squares. + let mut carry: u128 = 0; + for i in 0..6 { + let square = (a[i] as u128) * (a[i] as u128); + let low = (result[2 * i] as u128) + ((square as u64) as u128) + carry; + result[2 * i] = low as u64; + let high = (result[2 * i + 1] as u128) + (square >> 64) + (low >> 64); + result[2 * i + 1] = high as u64; + carry = high >> 64; + } + debug_assert_eq!(carry, 0, "adding the diagonal overflowed 12 limbs"); + + result +} + +/// Reduces a 12-limb (768-bit) value modulo `p`, per SP 800-186 §G.1.3 (see the module docs). +/// +/// **Precondition: `t < p^2`**, which is G.1's own stated precondition ("given an integer A less +/// than m^2") and holds for every call site, since `mul` is the only one and it passes the product +/// of two canonical (`< p`) field elements. The bounds below all rest on it; a raw 768-bit value +/// near `2^768` is *not* reduced correctly by this function. +/// +/// Bounds. Each of the ten terms is a 384-bit value, so `T + 2*S1 + S2 + S3 + S4 + S5 + S6 < +/// 8*2^384` and `D1 + D2 + D3 < 3*2^384`. Starting the accumulator at `4p` therefore keeps it +/// non-negative throughout (`4p > 3*2^384`, since `4p - 3*2^384 = 2^384 - 4*2^128 - ... > 0`) and +/// bounded by `4p + 8*2^384 < 12*2^384`, so seven limbs are always enough and the top limb is at +/// most 11. Folding that top limb back in via `2^384 ≡ C` adds `u_hi*C < 12*2^129 < 2^133`, +/// leaving `V < 2^384 + 2^133`; since `p > 2^384 - 2^129`, `V - p < 2^133 + 2^129 < p`, so exactly +/// one conditional subtraction finishes the job. +fn reduce(t: &[u64; 12]) -> [u64; 6] { + let a = |i: usize| word(t, i); + + // SP 800-186 §G.1.3's ten terms, each row read right to left from the document. + let t_term = term([a(0), a(1), a(2), a(3), a(4), a(5), a(6), a(7), a(8), a(9), a(10), a(11)]); + let s1 = term([0, 0, 0, 0, a(21), a(22), a(23), 0, 0, 0, 0, 0]); + let s2 = + term([a(12), a(13), a(14), a(15), a(16), a(17), a(18), a(19), a(20), a(21), a(22), a(23)]); + let s3 = + term([a(21), a(22), a(23), a(12), a(13), a(14), a(15), a(16), a(17), a(18), a(19), a(20)]); + let s4 = term([0, a(23), 0, a(20), a(12), a(13), a(14), a(15), a(16), a(17), a(18), a(19)]); + let s5 = term([0, 0, 0, 0, a(20), a(21), a(22), a(23), 0, 0, 0, 0]); + let s6 = term([a(20), 0, 0, a(21), a(22), a(23), 0, 0, 0, 0, 0, 0]); + let d1 = + term([a(23), a(12), a(13), a(14), a(15), a(16), a(17), a(18), a(19), a(20), a(21), a(22)]); + let d2 = term([0, a(20), a(21), a(22), a(23), 0, 0, 0, 0, 0, 0, 0]); + let d3 = term([0, 0, 0, a(23), a(23), 0, 0, 0, 0, 0, 0, 0]); + + // B + 4p = 4p + T + 2*S1 + S2 + S3 + S4 + S5 + S6 - D1 - D2 - D3, over seven limbs. `S1` + // appears twice rather than being doubled, so every step is the same 7-limb add. + let mut acc = FOUR_P_LIMBS; + for addend in [&t_term, &s1, &s1, &s2, &s3, &s4, &s5, &s6] { + let (sum, carry) = nat::add(&acc, &widen(addend)); + debug_assert_eq!(carry, 0, "P-384 reduction accumulator overflowed 7 limbs"); + acc = sum; + } + for subtrahend in [&d1, &d2, &d3] { + let (diff, borrow) = nat::sub(&acc, &widen(subtrahend)); + debug_assert_eq!( + borrow, 0, + "P-384 reduction accumulator went negative despite the 4p bias" + ); + acc = diff; + } + + // Fold the accumulator's top limb back in: `2^384 ≡ C (mod p)`. + debug_assert!(acc[6] <= 11, "P-384 reduction top limb exceeded its proven bound"); + let low: [u64; 6] = [acc[0], acc[1], acc[2], acc[3], acc[4], acc[5]]; + let (v, carry) = nat::add(&mul_small(acc[6], &C_LIMBS), &widen(&low)); + debug_assert_eq!(carry, 0, "P-384 reduction fold overflowed 7 limbs"); + + let (diff, borrow) = nat::sub(&v, &widen(&P_LIMBS)); + let mut selected = [0u64; 7]; + ct::conditional_select(Condition::::from_lsb(borrow), &v, &diff, &mut selected); + debug_assert_eq!(selected[6], 0, "P-384 reduction left a value >= 2^384"); + [selected[0], selected[1], selected[2], selected[3], selected[4], selected[5]] +} diff --git a/crypto/ec/src/p384_comb.rs b/crypto/ec/src/p384_comb.rs new file mode 100644 index 000000000..ccc1434c6 --- /dev/null +++ b/crypto/ec/src/p384_comb.rs @@ -0,0 +1,68 @@ +//! Fixed-base comb scalar multiplication: `[k]G` for P-384's base point `G`, with `k` secret. +//! Identical algorithm to [`crate::p256_comb`] -- see that module's docs for the full derivation +//! and verification methodology -- except `D * WIDTH` divides `BITS` exactly here (`384 / 6 = +//! 64`), so `FULL_COMB == BITS` with no zero-padding. +//! +//! Verified (not checked in) against 200 random `k` plus edge cases (`k = 1, 2, 3, n-1`), each +//! cross-checked against the SP 800-186 Appendix A.1.1 affine group law. + +use crate::p384::P384FieldElement; +use crate::p384_comb_table::{COMB_TABLE_X, COMB_TABLE_Y}; +use crate::p384_point::P384JacobianPoint; +use crate::p384_scalar::P384Scalar; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 6; +const BITS: usize = 384; +const D: usize = BITS.div_ceil(WIDTH); +const FULL_COMB: usize = D * WIDTH; +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]G`, branch-free in `k`. +pub fn comb_multiply_base_point(k: &P384Scalar) -> P384JacobianPoint { + let limbs = k.limbs(); + let mut r = P384JacobianPoint::INFINITY; + let top = (FULL_COMB - 1) as isize; + for i in 0..D { + let mut secret_index: usize = 0; + let mut j = top - i as isize; + while j >= 0 { + let bit = if (j as usize) < BITS { + (limbs[(j as usize) / 64] >> ((j as usize) % 64)) & 1 + } else { + 0 + }; + secret_index = (secret_index << 1) | (bit as usize); + j -= D as isize; + } + r = r.double(); + let (x, y, is_infinity) = table_lookup(secret_index); + r = r.add_affine(&x, &y, is_infinity); + } + r +} + +/// Looks up comb table entry `secret_index`, scanning every one of the [`TABLE_SIZE`] entries +/// under a mask. See [`crate::p256_comb::table_lookup`]'s docs. +fn table_lookup(secret_index: usize) -> (P384FieldElement, P384FieldElement, Condition) { + let mut x_limbs = [0u64; 6]; + let mut y_limbs = [0u64; 6]; + for i in 0..TABLE_SIZE { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + let mut next_x = [0u64; 6]; + ct::conditional_select(is_this_entry, &COMB_TABLE_X[i], &x_limbs, &mut next_x); + x_limbs = next_x; + let mut next_y = [0u64; 6]; + ct::conditional_select(is_this_entry, &COMB_TABLE_Y[i], &y_limbs, &mut next_y); + y_limbs = next_y; + } + // Entry 0 is the point at infinity; every other entry is affine (Z == 1). The accumulator's + // `add_affine` takes the identity as a flag rather than as a `Z == 0` coordinate. + let is_infinity = Condition::::is_equal(secret_index as u64, 0); + ( + P384FieldElement::from_internal_limbs(x_limbs), + P384FieldElement::from_internal_limbs(y_limbs), + is_infinity, + ) +} diff --git a/crypto/ec/src/p384_comb_table.rs b/crypto/ec/src/p384_comb_table.rs new file mode 100644 index 000000000..41f947729 --- /dev/null +++ b/crypto/ec/src/p384_comb_table.rs @@ -0,0 +1,586 @@ +//! The checked-in fixed-base comb table for `[k]G` (P-384's base point `G`), width 6, same choice +//! as [`crate::p256_comb_table`] (the standard width for fields over 250 bits). See +//! [`crate::p384_comb`] for the multiplier that uses this table. +//! +//! `COMB_TABLE_X[i]`/`COMB_TABLE_Y[i]` are the affine `(x, y)` coordinates of the `i`-th table +//! entry, as little-endian `u64` limbs; entry `0` is the point at infinity (`x = y = 0`). +//! +//! Regenerated and compared against these checked-in values by this module's own `tests` +//! submodule below -- see [`crate::p256_comb_table`]'s docs for why this is a unit test, not an +//! integration test. + +pub(crate) const COMB_TABLE_X: [[u64; 6]; 64] = [ + [0, 0, 0, 0, 0, 0], + [ + 0x3a545e3872760ab7, 0x5502f25dbf55296c, 0x59f741e082542a38, 0x6e1d3b628ba79b98, + 0x8eb1c71ef320ad74, 0xaa87ca22be8b0537, + ], + [ + 0x06ba5918d74d9642, 0x92dd9cdedfa0f56d, 0x74c1ac3d8cbae3ef, 0x2fce93bf5f6f39bf, + 0xff348797e2cfe67c, 0x079dc566510cc718, + ], + [ + 0x371fdb782c009246, 0xbcb6f706b7ebf317, 0xe753edd8cf2cbc3d, 0x852cc3aba37b7552, + 0xac8476f641789de5, 0xbda8cbb352e85654, + ], + [ + 0x84a2ae350020362e, 0xea2b1df419bf5f49, 0x2bb64e18287c9b53, 0x11c2df652b7b03e9, + 0x23a42ea485d5a524, 0xe10cb36d11cd506e, + ], + [ + 0x981ffb59ae569de6, 0x586ea8878091accf, 0x67fc329408a70830, 0x7e6ee5f4849e299f, + 0xc79cf82450b92155, 0xde3e77fbc2010434, + ], + [ + 0xf6da53e1780e56f3, 0xfd5f2fb0af4d2766, 0x52922de8e383bf0d, 0x1c92aba058ee4ddc, + 0x3b2384c0238e7643, 0x2d350fa20d6ac898, + ], + [ + 0x16273979b29adc60, 0xa30c604d3be4714f, 0x5a2a19fa4019bd55, 0xef8d50a1431ef84d, + 0x581b7cfa417c18a3, 0xadd3b7e9c4245900, + ], + [ + 0xa628b09aaa03bd53, 0xba065458a4f52d78, 0xdb2987894d10ddea, 0xb42a31af8a3e297d, + 0x40f7f9e706421279, 0xc19e0b4c800119c4, + ], + [ + 0x30991560aa133909, 0x9097dbb1c6cb0017, 0xd37de424b860fae6, 0x9bb183b270b375dd, + 0x567a6233cd6ce3a3, 0xaab8bb9f0fdc3088, + ], + [ + 0xf5b4350f4ab018d9, 0x03d42bfa4f890f56, 0xa59802ab472abcbe, 0xc49dc552ab00b039, + 0x5bdbfa1b4905edeb, 0x8b714479aadaf829, + ], + [ + 0x50e97c63b6d82f46, 0xde6337d914b98cd9, 0x387bcead46b80210, 0x2333e9a7662d4782, + 0x4fb348a89493f992, 0x1a2f720851ac4ae9, + ], + [ + 0x50938b9b09331ee2, 0xb5ea6c1801c84dea, 0xbe617a0fd5a6bd1f, 0x78bb665f8f79292b, + 0xb942f76558e00408, 0xf8b838b2866f2b6e, + ], + [ + 0x4a286f3303034968, 0xfb4df38c05a7db5d, 0x3b640deca8b1928e, 0x5d1b884bd0fdfc4e, + 0xf57371ce69519c83, 0x5f63461a23145ab8, + ], + [ + 0x06f6cff74fee705e, 0x315fc16f4b772c5b, 0x4b2fdf3568592ff1, 0x0ad35e83e9bcbebb, + 0x1bbcc1e0690b01a1, 0x69f7b65f0774ea13, + ], + [ + 0x0b68515ff756ac51, 0x03ac4e13bc1de0c6, 0xcbba88d2e6c5f5b7, 0xeefa675823280efa, + 0xe415dfc4d84b809e, 0x03c2592ce6580ef1, + ], + [ + 0x31efc1321a9bb0aa, 0xda74bdf2bb0f6ca7, 0x1af000f8cd3d0497, 0x39acb8f22aaa3451, + 0x23eaf4cdedfb898d, 0xe7b3f42dcad3d109, + ], + [ + 0xf2977c54d2529ad2, 0xc55087d9ed6e33e0, 0xb8831540167e355a, 0xfeea9103aba42b33, + 0x90d05905ef3947c1, 0xaf085311760d24bf, + ], + [ + 0x1d12ee6d45f5b090, 0x4b71091f485b3a1f, 0x86d41f81f5b0b2a7, 0x0b230b2a2613a770, + 0x57d0106cd7c428db, 0xf277b4bc9df04a3c, + ], + [ + 0x56c0cb845615db79, 0x9ed4f5c9080edb56, 0x07d5137c1ad062e6, 0xc0a02132e053ad7d, + 0xbbd20e23218138c5, 0x71a9821ed005511c, + ], + [ + 0xcd8127fdabe86f94, 0x1d082a02c4aa703d, 0xe4c033a6856805e7, 0x7f9803ea4cffcbdf, + 0x08f01be4456f325c, 0x14e4e4d056899a2c, + ], + [ + 0x63b6db017dfa617b, 0x2382c6f43958c611, 0xd8aacd08807a5a47, 0xf468a53c3fc68cda, + 0x8ad836ae17d5f5bb, 0x91da6c57c592adc7, + ], + [ + 0xbb31f60387667e11, 0xfd4f93f57e745458, 0xbf08a73c309a7d39, 0xe7e2eef323ad2f18, + 0x0cad330540360944, 0xa2b3aca748664803, + ], + [ + 0x5cbc04229d6b0c98, 0xde8a0e6a2f796386, 0x747f91b8023f6729, 0x506bf433a06414fc, + 0x453f88b88771441c, 0x155344c505852b1b, + ], + [ + 0xad7f8aec9e3303a4, 0xadcbca0ffb283ba5, 0x7ba9925103fd29c7, 0x2e31cf2a735503ef, + 0x60d66a8a5dad58fc, 0x9c05081572f7fc70, + ], + [ + 0x698fe08d110ec812, 0xb1bcb75f6c797f59, 0x96eebb123c83e188, 0x17b971411bc4c22a, + 0xf28850891dbbeeb0, 0x3f92deeb74551cfc, + ], + [ + 0xb5406aebffdcdece, 0x505d7d431ac67153, 0xd4cd49910433839b, 0x3d981f0054540a3f, + 0x055f14e6036e1cf2, 0xfc45acb0c33a23e5, + ], + [ + 0xfaff6b3f0aacbdbe, 0x711d70d91474e4e0, 0x2b9618d8bb71b0f2, 0x01eeb5809019b8da, + 0x6f034817a6202191, 0xf13420946dbb1de3, + ], + [ + 0xd165cd219c8e4278, 0xd464f8babe3074cf, 0xe880163d596c7e37, 0xff21cf573b71cdbf, + 0x104c1028abb38eac, 0x86d1bdcf092ee0c5, + ], + [ + 0x6d0c4d55c2ce683e, 0x47aed6903293209b, 0xae73eb9e43ab78dd, 0xbff13ca683c397ed, + 0x4bfa26086625250f, 0xac3bec825d44fb41, + ], + [ + 0x9857f321b002667c, 0x9fc753065f935341, 0xff0b96fa7469adf5, 0xb409ff5bfaab0698, + 0x39e820326d15c913, 0x86dc3fa0eb2cfb46, + ], + [ + 0x2c06d1a84f70bddb, 0x50fa35fc682e4e19, 0xebc25ff2ed755360, 0x9322e2059f6b8739, + 0x7c6d9e03b7a81a4f, 0x3a9e39a88869d0cb, + ], + [ + 0x110162f2968b4a5f, 0x1323feffba0f8b3d, 0x89acc4c0b1666eaa, 0x85dfb3833007a482, + 0x5f5449e75c7a9ba1, 0x38174a006bfcf21e, + ], + [ + 0x94971913df13f515, 0xfe21826633b3f908, 0x0e7643231639fcf5, 0x2f1c9564a1254809, + 0x28a0ded95715ac43, 0xcc2491483db85f16, + ], + [ + 0x3741a3d0daf70cc5, 0x4f89c2561949dca9, 0x84d76e8e4209b8bb, 0x89d03a2861aa033d, + 0x2bb8225a62eb53a5, 0xf16caec4dd131005, + ], + [ + 0x564f510b12cd19bd, 0x055916a4243d4b30, 0x017c752e59d3ebac, 0x4c292ed9a5b4b98c, + 0xee06b30275051686, 0x234ff191799f7cab, + ], + [ + 0x0a289321ae3c5c76, 0xd35fbdc6a1a23a44, 0x72bd5e9127340d10, 0x38df4f70fca6aa42, + 0xd9f2612f50546b32, 0xc38197025c98fdf6, + ], + [ + 0x1e9910c1370fe603, 0xfea601805d9dad61, 0xeb38209d2a100fb0, 0x32a7a05acecb606c, + 0x2d74e5bfdf14cfba, 0xc65d0259dd1a12b5, + ], + [ + 0x217affc5535b9874, 0xd0f3da4ad7396b2b, 0xc2f3120a5189f1d9, 0x640d40a513be9d9d, + 0xc65aeec07ea25c0b, 0x148ac3c9ac1b42f6, + ], + [ + 0x54372e4324c2bb10, 0x3ff377fc35cef78a, 0x6b9d764ad5963475, 0x220b12a8ea0f3bb8, + 0xfb9ed41c2d5d3f66, 0xdcafab6ec7dd2e7d, + ], + [ + 0x69fe6de0f2185961, 0xac246678cab2f970, 0xeaa8fdc7d1fa56cb, 0xf46115f695e4ca40, + 0x408b8231884b0b69, 0x740a16f9861cc448, + ], + [ + 0x605ae87e66b45964, 0xdac9d9adb27e5ca5, 0x2e078313a3499782, 0x6bb00d3513c7265a, + 0x2c18934bfaab601e, 0xae61e44c311c2d54, + ], + [ + 0xa7336b4c292db8d6, 0x0d03e76ffb3f71a7, 0xcff0e362b272dedf, 0xba4575e346e3fe2c, + 0xb38acd7ae7055f43, 0x8510fd43612db2a9, + ], + [ + 0xb69e1a4130295b6d, 0x42cba75b3183ca76, 0x8931644d44504121, 0x183ca5975dc284cf, + 0x2c475a8efda50ff0, 0xba0bb1402d02587c, + ], + [ + 0x9cb262051d808bf2, 0x82dfbf8d7fa50c45, 0xf7615a8bfe6665b4, 0xfd616cc2b0ceb476, + 0xd1bbf01d0b9af731, 0x2d429b8f322c4cc3, + ], + [ + 0x3c8aef7b50558a7e, 0xb691632348231bfe, 0x9ac79b243c54040d, 0x499ffffb84c33825, + 0xcb5c8a686de02a66, 0x63af0da1855393c4, + ], + [ + 0x94dc664cef74f723, 0x7bc91861cbbaff21, 0x2690c7841ae6dfc8, 0x4df56fbcdf65ae68, + 0xdcb40a65a3730712, 0x2827bf67b51d6a88, + ], + [ + 0x1cb12191b59f7464, 0xc1af45ef11e1ed1e, 0x1f6572bcb2be31e0, 0xb5421ece778c4bb1, + 0x559986d9b9aac9ff, 0x6d0b354ec6802365, + ], + [ + 0x2fdcb3139de8a87c, 0x5b9307385a427d95, 0xa2b8e9aa5b8cd3f8, 0xeb1c24abc983dfc7, + 0xfb9fd3ce88113586, 0x66c8e65285121157, + ], + [ + 0x495175cd522a423f, 0xefc6442b04e53690, 0x30b583071316ee5c, 0xc6d30519a2c65976, + 0xd6eab70916a88bb6, 0xefd0fdb5af14ee70, + ], + [ + 0xe5b1a0d57852a8b0, 0xd2e22ec0d0ff8140, 0x0da6b7a875476490, 0x53120134da4a513c, + 0x78fbb953aff54c16, 0x3bd530a3e2a40faa, + ], + [ + 0xc0e60ab1664aa28d, 0xceb2995e9d940c86, 0x5530b3b2ca4003ad, 0x42e21e3427487874, + 0x82e36dc1ac5b72ae, 0x0b4b5eeacb46ab57, + ], + [ + 0x253e8c05193f489a, 0x24bdf941188e2307, 0xa6ccfb9bd2a64c9b, 0x26e0868d1a71a7e6, + 0x89a2a4fd1d99dfe4, 0xa012e8cb12095cf4, + ], + [ + 0x657e15732740d819, 0xd4c8400f7e53bbf6, 0x045e5aef731937ea, 0x5c97ac7e492b739c, + 0x21d2e70e1ec3b215, 0x3ed621acddd6088a, + ], + [ + 0xfaf24e1d21228ab2, 0x78debe84e1452df2, 0xbef7d9249e7f135a, 0x149f1f4ae7f8d0e0, + 0x128b813d2762544e, 0xebda14bf9c390a58, + ], + [ + 0x9369aae4484ce8d6, 0x7b1a9637fc277a27, 0x5c4613cfa530e76f, 0x84564d2374afd88c, + 0xf083971b14443d32, 0xf73898fd5fd747fa, + ], + [ + 0x81a55fa1241f665b, 0xa0ea609264634b7e, 0xd0c389c4eb475504, 0x2cd1c98f8d1e0203, + 0xf359afab84a47688, 0x59bcd3467a41c64c, + ], + [ + 0xacd8e2510d46066d, 0xa6cda0796f1d5714, 0x419ef5adca36fbdf, 0xdc48951d9e1d7a6f, + 0xc6c22c15b7a52681, 0xb6c9dc11aa724286, + ], + [ + 0x7765ebe9b0e01600, 0x1492ac1925c62afc, 0xf914062a16db2db3, 0x2c552735f9417106, + 0x675b4089945f1dca, 0x1cb8dfe59590219d, + ], + [ + 0xc12065e71ca916b6, 0x2cbda6c0f8325358, 0x6f26cc3157e6f0b7, 0xbc17d3341c54e327, + 0x9f1ee7cdfbedaef5, 0xb61f0040341c165a, + ], + [ + 0x34c95b844bfb949b, 0x4a1156c02f8bac59, 0xbfb266eb95f2a511, 0x333c1450528f0a1d, + 0xc23dc688361d12b2, 0x6575994a3bc7fa44, + ], + [ + 0xef16edc565994991, 0x2b6f4ea7f39a4279, 0x55074ed7c07e0a7b, 0x3415b9a669317ad6, + 0x0cf8b93fa472e0a8, 0x1237dea28532752b, + ], + [ + 0xedfa66b6ebc7a05a, 0xe12f0b82d005432c, 0xefbc796e9051e2a6, 0xc6658e671a964862, + 0xc2b421592d6041d1, 0xc0f39509557cca45, + ], + [ + 0x03995fe72c3fc486, 0xff565aadb6b72bf0, 0xbfbae8c848a40004, 0x9c99cd5a13b5a1e8, + 0xfee77ee2fcad95b0, 0x0b63539934d16b96, + ], +]; +pub(crate) const COMB_TABLE_Y: [[u64; 6]; 64] = [ + [0, 0, 0, 0, 0, 0], + [ + 0x7a431d7c90ea0e5f, 0x0a60b1ce1d7e819d, 0xe9da3113b5f0b8c0, 0xf8f41dbd289a147c, + 0x5d9e98bf9292dc29, 0x3617de4a96262c6f, + ], + [ + 0xa0ecd5d0c16bb523, 0x29f49d50cc51c42f, 0x36275538892459a9, 0x626377708484b5f1, + 0x89a8437e5c806f88, 0xa64da09a5d9087bc, + ], + [ + 0xda1b89b6a4aef26d, 0x4c50f67c1cce0f01, 0x7ad4c74de690eb4c, 0x946099018a2d49d1, + 0xd907b06a9b9b805f, 0x23bc95be59eea2c2, + ], + [ + 0xe5f50bfe2adaefc2, 0x64666b5536adb53e, 0x7768fb7fe90ed2a4, 0x425d781706cc84c0, + 0x52ec806a31d6ab0b, 0xd7e679724697d363, + ], + [ + 0xa6ec9dc7e3ee3800, 0x8e5e92fa43cf88b6, 0x757e493bf843cae9, 0xe1c2864517ebe71c, + 0x00d19ce032576992, 0xe30df4b84db810d9, + ], + [ + 0x2929929efb35aa8b, 0xa8743694a6e54797, 0x67f4174cc525c526, 0x8cf8b3c9e0e2f34d, + 0x80ccbce70b8f32ee, 0xf15779e6689de2fa, + ], + [ + 0xb76f4554633d01bb, 0x34421d86a319b27b, 0x711a529218870284, 0xfe371b185db1186c, + 0x3040af18ba6174f4, 0x6fb0070f523c7dda, + ], + [ + 0x822d0fc5e6c88c41, 0xaf68aa6de639d858, 0xc1c7cad135f6ebf2, 0x577a30eae3567af9, + 0xe5a0191d1f5b77f6, 0x16f3fdbf0356b301, + ], + [ + 0x16c5b981600ad5a6, 0xebdf73f2d62faa44, 0x6d955bb3c9747bf3, 0xf6005fc815eb04ac, + 0xf0af01d1282050b5, 0x48942f81314f6d28, + ], + [ + 0xb69f612110903577, 0xffcf3103caa17043, 0x194befcc12b7d6f0, 0xc2315ca244fa539f, + 0x330a336c27d7aa39, 0xd67b41415e7e1a8e, + ], + [ + 0x9778e181f9a0adda, 0x171a79bad8639a3a, 0x2d7d5d5a7a7b3134, 0xb7e009194b129683, + 0xec7e6e4eee578a55, 0x08d8dab3dc699d11, + ], + [ + 0x2a518d483780eda2, 0x2a58627f5f9fcb09, 0x48f510fa8eb96eb4, 0x97dded6d15802f8f, + 0xe75cd28daaaba84b, 0xacd28afa7ede1244, + ], + [ + 0xedefcd1844704cbc, 0x924d7e7e64dd49c2, 0x2cd63fd0097c32a2, 0x3e547a0c654fe06d, + 0x8c5a79b44afbfa16, 0x749abcda2b1b5d04, + ], + [ + 0xeaa68fc868fbc242, 0x749fa4694c78bcf9, 0xcf3af279cbbdfc33, 0x50f52cde020525cc, + 0xcebe195964cbedc0, 0x1d8d9c443df78387, + ], + [ + 0x0b222b169927269b, 0x38fc9b2ac38ff3ba, 0x1b3c9efe3d832efc, 0xb53849c12dcd5708, + 0x01ea1f89338b4295, 0xeb3ebb50d7131daa, + ], + [ + 0x98ca61b55e695394, 0x6f5911020e70a633, 0x80a93ec7216b71d5, 0xbbb25c8fc463e3b8, + 0xb18e3fba8615e7a5, 0x5a5d5c5ffc212920, + ], + [ + 0xe5b14850bddd2f1f, 0xf6eb147855841c80, 0x04c7f6afa63e5ed9, 0x3b9541ca65e211e7, + 0x2d06224de6d883ae, 0x974628aa2a257ffa, + ], + [ + 0xfc506e660a32a7e5, 0x846ce285d317047c, 0xceb4d115cc394c36, 0xef860bec97182db4, + 0x772d38d72f49ed16, 0x0cc65d7427e042ea, + ], + [ + 0xb9b7aac6bf27c3a7, 0x461746b9df4565cb, 0x1cca5db1b23f38f5, 0xb848d7e9636c2988, + 0x9905c28c99d7f1bc, 0xd57ee56d825ee66d, + ], + [ + 0xc220c4e368450d6a, 0x7100595d348c7c8a, 0x42734e23260eb12a, 0x6d1b51f85c2db9e8, + 0x983e3bafda5a3ab7, 0x997c7daa8e12b7e4, + ], + [ + 0x848c7840c01767e8, 0xb27e5cdaaaf49f7e, 0x67a064723e42db96, 0xa8e8e3431741f927, + 0x0305167e43ccdd66, 0xe590418f48cf4b36, + ], + [ + 0x87675ce3ba1fce75, 0xbb409ff64d64d1cd, 0x0f13e3e4de117c49, 0x31d85ebc7414266b, + 0x8ca52dd3271ab0fe, 0xc22174f5964295df, + ], + [ + 0x2ee3731cd1fe2b3e, 0xd29d1c69d3a13366, 0x04941baeb6ba5ae0, 0x453ab0575c9074af, + 0x8d1d542420932fdd, 0x98495d67719d78fa, + ], + [ + 0x35f5c4f6c76bfe85, 0xbb04ba184cafb0e7, 0x8d19a621828c22d7, 0x50e7bd15fdef9916, + 0x2185491f8b0c751c, 0x1cf397cddb05f5aa, + ], + [ + 0xb2b3d0190692389c, 0x81c1e25fa326992a, 0x8f2426b6c3fdfc75, 0x964b64b749e7adaf, + 0x30b6fe07ad954e6c, 0x1008a9192de7154f, + ], + [ + 0x04b8d16b1d129997, 0x086062f1bb8b42c8, 0xaebcf8a040aeccee, 0x139ccdbfb96d5265, + 0xba4828740134a1d2, 0xce949a4c5f0e60e7, + ], + [ + 0xe2be4e8e4b678259, 0x2d2b5523f877dabe, 0x51742d3525de025c, 0x04adba390bb80cd0, + 0x1be43de14d280ea6, 0xb08dc4b8d40aec5d, + ], + [ + 0x48f6c554cd619224, 0xcbdda09fc9b564e6, 0xd016002040245ee6, 0x058df586386aae10, + 0x8052fb318fcf7688, 0xf957daebea5b86c3, + ], + [ + 0x69d09904b5ea9058, 0x8745fdeba022f24a, 0x5beefafc3281a45d, 0x1d5c41776aa4bb27, + 0x50b310f04faa1ea1, 0xe6d7909e14137409, + ], + [ + 0x69bee69d3e1e3fd2, 0x2cdbf99d66c61037, 0x223fa09fc1b13717, 0x1d4e2d2073840224, + 0xf8d1807f18b74223, 0x107a3bd91190c01c, + ], + [ + 0x150824ecfe68eff8, 0x7434b70d924b06a1, 0x45e44aa01addf43e, 0xd3d6bbce4f4f63c3, + 0xacb7733fdf8deecd, 0x53d9814d80b606df, + ], + [ + 0x9f7dc2544f0474fc, 0x13f390d740cb718b, 0xe6091c183a45ad17, 0x9b795d801605976d, + 0xe0bcd7af2055ee90, 0xd637fbf692ee7c2e, + ], + [ + 0xc4bd32d154c8f22d, 0x20f4d89daaf03b8a, 0x2d25ac666cbbc34f, 0x1dde41e1b210256f, + 0x982abc37b830e83c, 0x859f781e4a0157ba, + ], + [ + 0xfb3da605b3f85850, 0xa894e8519705c612, 0xd3c477f5b6e52255, 0x275b8dc4609de217, + 0x836e288617ccd6d8, 0xbefeb4747626b688, + ], + [ + 0x7730ffe1847c9865, 0xcdbd40555fc2e271, 0x5724c2c43f876569, 0x4824d115600a300e, + 0x1dafb1aeb6d99957, 0xf3b9740fa2ee809b, + ], + [ + 0x6a2544df7476a566, 0x5df6ecff9a396e7f, 0x4522f408ead8f18c, 0xcbf8cda23a8c0e3e, + 0x3be9abf251c4a7d1, 0x634cd141ad0790ea, + ], + [ + 0x7b4d13b6f5f8c9c8, 0xdc79a2b393b145ed, 0xcc9c29e6f7cff674, 0x590a59bc80655087, + 0x0cf046a23524f00e, 0x676256619dc026bd, + ], + [ + 0x949c94d47c4c23e0, 0x4c25d7a675b629e8, 0xbaa85bfcfa279b86, 0x3e066e812a18aafd, + 0x9f3ab93d51bbaead, 0x1665215e095c11e2, + ], + [ + 0x598e839a0c7bb28c, 0xc319327992905130, 0x8fa52ccfd0f989ee, 0x819d4150de21507b, + 0xe3a8959d9e2b7de0, 0xaa5f97888720a5d8, + ], + [ + 0x7c2024cde63b89dd, 0x4339955515b8bc28, 0x3a25efb9e993ccad, 0x0e01859a35dbb862, + 0x916982ea3521322c, 0xd25b20d619f5f297, + ], + [ + 0xc5916e59511e4dde, 0x60cc36ed514d5438, 0xc5332778c756a818, 0xbc6ac88bc995b742, + 0xeef9e1b39613e95e, 0x050394c35eb046f5, + ], + [ + 0x189c8dcd9bc4aad3, 0xdb5d782bcbfb868a, 0x5b915b8e37ef7c9a, 0x05698ebc702c0828, + 0x687beab162d4bf9f, 0x3f00c98a66f63c3e, + ], + [ + 0x994323ce678c33da, 0x2a5b1f4472475384, 0x18638850a4d9c1ae, 0x31c400736975f27c, + 0x69d8001adf1d904b, 0xef6270a84889b105, + ], + [ + 0x8b877cc0943b6ffb, 0x931b079026857e49, 0x61613d737db914fc, 0xe7c95092325e0b06, + 0x4236049c91c5a80c, 0x8a6c523ced476d14, + ], + [ + 0x5106d00b4c95881b, 0xe0b4c8a2498daf92, 0xca87438809537675, 0x0ab4215e3f77a490, + 0x77c191cc64869e35, 0xb15b7dc3cb5617c1, + ], + [ + 0xd7acb37483dfac3c, 0x45b47c9cc9c0488c, 0x5c9bac550913903c, 0xba001680669f2bc7, + 0xed4e6d4e187805ae, 0xca2e18bd98eabbc9, + ], + [ + 0x231d32390aa2d847, 0x054a9a12f186c861, 0x01bc8e70aa7cf18d, 0xc8a58bdd8f470162, + 0x96f5c7abf42108db, 0x1d880fc495a5760a, + ], + [ + 0xd83be65684a0210f, 0x590340619535d285, 0x18caf84784061f0e, 0xbab7a9a860314751, + 0x93a603c9e22cd7be, 0x1eea699752b5e890, + ], + [ + 0x75666e3e71ca7c18, 0x7fc491e53f0e31be, 0x2b5837953ec23d8e, 0x66eeeff303021f01, + 0x24e8cd38fc1a5b28, 0x716821c685425712, + ], + [ + 0xa46d2742087f83d8, 0xc47f329c7ce1bc46, 0x68c4944fca917d28, 0xe0d8b028ee1c8c08, + 0xed86abe020dbf9be, 0xd8b9040a7aae428a, + ], + [ + 0x522a9f514ead08fc, 0x02ddcfd64c7d5c68, 0x7cd9108be02676d8, 0x1742d25f4e23a730, + 0x28342177eca4e5d1, 0x2fc3854aea65145c, + ], + [ + 0x425a5c1b1bd5ee20, 0x429411f5212c5b5b, 0x100fb60f7d1a996e, 0xd429d128ed4e5b27, + 0x1261f141cda181a2, 0x763abfc494c8b1bb, + ], + [ + 0x3f38f26d6dbe26a9, 0x893a5049a7316c08, 0x5a30c2d2f5c699c3, 0xb2e9978b8bf6e42a, + 0x0b5415fc0db721dd, 0xf29d6bcda41c8365, + ], + [ + 0xf4c4561f2d14fe1b, 0xae8cfc1cb3f24f5e, 0x68e81b1e9b76389a, 0x8f9ddd405e63e131, + 0x3fdf4533734115cd, 0xf2b4af6d2d23cafe, + ], + [ + 0x15c17bfd366473a3, 0x2d899447d2325dea, 0x0465c806ac30e951, 0x534961f71bb96cfa, + 0xb1a6da804332ffd7, 0x21ce35950104f8ac, + ], + [ + 0x3aa100e8e64bc28e, 0x6e9eea3fd156e143, 0x9c5a311dc8c5471f, 0xf0e607ebe8940bf8, + 0x4cdd06d1c47a8f0b, 0x5106caa8cd527795, + ], + [ + 0xd4ab3c62a7646c3f, 0x80a8c060d8446a48, 0x46893c5712587682, 0x2cd0b8cbce834f82, + 0xdffaebadb004849a, 0x3e654e4441c3a434, + ], + [ + 0xcef9bdca41fab175, 0x349aa2d4e236b88d, 0x07aaeab6466b69d0, 0xb899491cc27dd5d2, + 0x81f425475099c372, 0x5d60f35e510ef1a2, + ], + [ + 0x091a3bd4c200326a, 0x44164a0c8f99e034, 0x622994d41b187383, 0x6732749db12da429, + 0x668b62370cb1f4bf, 0x42b6a335e2daffc9, + ], + [ + 0xc45d8db0c2cce9ed, 0x14d197feded49c21, 0x56c11eb8b2c46c12, 0x6b7f93718add12fc, + 0xc3c27c163b5bd8e4, 0xad81ee0340d43df2, + ], + [ + 0xf4a44125cf0dc0bc, 0x4b8385c348b2fc9b, 0x629a7a2d580bbe2b, 0x2c4f741b1881f3c5, + 0x1ea38b160aa343e2, 0x7735a28d7b81184a, + ], + [ + 0x42486f739509f4ce, 0xfa10ba0dd60fb23c, 0xf55a55a922d97e5c, 0x8ef47b078d750955, + 0x1e61b4103f540aa1, 0xfa6f0eb0c56b7257, + ], + [ + 0x2801ab2b5d778a19, 0x604d8349d5d0854e, 0x7aaf003b3091c681, 0xc9e2b6e10761ae3a, + 0xa5f0b69bfe414395, 0x419e8889adae97b4, + ], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::p384::P384FieldElement; + use crate::p384_point::P384JacobianPoint; + + const G_X: [u64; 6] = [ + 0x3a545e3872760ab7, 0x5502f25dbf55296c, 0x59f741e082542a38, 0x6e1d3b628ba79b98, + 0x8eb1c71ef320ad74, 0xaa87ca22be8b0537, + ]; + const G_Y: [u64; 6] = [ + 0x7a431d7c90ea0e5f, 0x0a60b1ce1d7e819d, 0xe9da3113b5f0b8c0, 0xf8f41dbd289a147c, + 0x5d9e98bf9292dc29, 0x3617de4a96262c6f, + ]; + + const WIDTH: usize = 6; + const BITS: usize = 384; + const D: usize = BITS.div_ceil(WIDTH); + const TABLE_SIZE: usize = 1 << WIDTH; + + /// Regenerates the comb table from scratch via the crate's own (independently verified) + /// point arithmetic. See [`crate::p256_comb_table`]'s identical test for the construction. + #[test] + fn regenerated_table_matches_checked_in_constants() { + let g = P384JacobianPoint::from_affine( + P384FieldElement::from_limbs(G_X), + P384FieldElement::from_limbs(G_Y), + ); + + let mut pow2 = [g; WIDTH]; + for i in 1..WIDTH { + let mut p = pow2[i - 1]; + for _ in 0..D { + p = p.double(); + } + pow2[i] = p; + } + + let mut table = [P384JacobianPoint::INFINITY; TABLE_SIZE]; + for bit in (0..WIDTH).rev() { + let step = 1usize << bit; + let pw = pow2[bit]; + let mut i = step; + while i < TABLE_SIZE { + table[i] = table[i - step].add(&pw); + i += 2 * step; + } + } + + for (idx, point) in table.iter().enumerate() { + if idx == 0 { + assert!(point.is_infinity().to_bool(), "table[0] must be infinity"); + assert_eq!(COMB_TABLE_X[0], [0, 0, 0, 0, 0, 0]); + assert_eq!(COMB_TABLE_Y[0], [0, 0, 0, 0, 0, 0]); + } else { + let (x, y) = point + .to_affine() + .unwrap_or_else(|| panic!("regenerated table[{idx}] is unexpectedly infinity")); + assert_eq!(x.to_limbs(), COMB_TABLE_X[idx], "table[{idx}].x mismatch"); + assert_eq!(y.to_limbs(), COMB_TABLE_Y[idx], "table[{idx}].y mismatch"); + } + } + } +} diff --git a/crypto/ec/src/p384_domain.rs b/crypto/ec/src/p384_domain.rs new file mode 100644 index 000000000..ed121557c --- /dev/null +++ b/crypto/ec/src/p384_domain.rs @@ -0,0 +1,22 @@ +//! P-384 domain parameters, NIST SP 800-186 (Feb 2023) §3.2.1.4. `p` and `a = -3` live in +//! [`crate::p384`] (the `a = -3` shortcut is baked directly into +//! [`crate::p384_point::P384JacobianPoint::double`]); `n` lives in [`crate::p384_scalar`]. This +//! module holds the rest: the curve coefficient `b` and the base point `G`. + +/// The curve coefficient `b` in `y^2 = x^3 - 3x + b`, little-endian `u64` limbs. +pub const B_LIMBS: [u64; 6] = [ + 0x2a85c8edd3ec2aef, 0xc656398d8a2ed19d, 0x0314088f5013875a, 0x181d9c6efe814112, + 0x988e056be3f82d19, 0xb3312fa7e23ee7e4, +]; + +/// The base point `G`'s `x` coordinate, little-endian `u64` limbs. +pub const G_X_LIMBS: [u64; 6] = [ + 0x3a545e3872760ab7, 0x5502f25dbf55296c, 0x59f741e082542a38, 0x6e1d3b628ba79b98, + 0x8eb1c71ef320ad74, 0xaa87ca22be8b0537, +]; + +/// The base point `G`'s `y` coordinate, little-endian `u64` limbs. +pub const G_Y_LIMBS: [u64; 6] = [ + 0x7a431d7c90ea0e5f, 0x0a60b1ce1d7e819d, 0xe9da3113b5f0b8c0, 0xf8f41dbd289a147c, + 0x5d9e98bf9292dc29, 0x3617de4a96262c6f, +]; diff --git a/crypto/ec/src/p384_extra_bits.rs b/crypto/ec/src/p384_extra_bits.rs new file mode 100644 index 000000000..c477754d5 --- /dev/null +++ b/crypto/ec/src/p384_extra_bits.rs @@ -0,0 +1,64 @@ +//! FIPS 186-5 Appendix A.4.1, "Conversion of a Bit String to an Integer mod n via Modular +//! Reduction" -- the "extra random bits" method both `bouncycastle-ecdsa`'s `keys_p384`'s key generation +//! (Appendix A.2.1) and `bouncycastle-ecdsa`'s `ecdsa_p384`'s randomised per-message secret (Appendix A.3.1) +//! use to turn a DRBG output wider than `n` into a value in `[1, n-1]`. Identical algorithm to +//! [`crate::p256_extra_bits`] -- see that module's docs for the full reasoning -- with P-384's own `n` +//! substituted. +//! +//! # Why P-384 draws extra bits at all +//! +//! Not for bias. P-384's `n` is within `2^190` of `2^384`, so Appendix A.4.1 step 2's bound +//! (`2ρ(1-ρ)(n-1) <= ε·N`, `ε = 2^-64`) already holds at `l = 384` with a bias near `2^-194`, and +//! FIPS 186-5 Table A.2 accordingly gives `p384` a required and recommended DRBG output of 384 +//! bits for key generation. An earlier version of `bouncycastle-ecdsa` drew exactly 48 bytes for both key +//! generation and the randomised per-message secret on that basis, and reduced with a single +//! conditional subtraction of `n - 1`. +//! +//! Appendix A.3.1 is stricter than Table A.2, though. Its step 2 returns FAILURE if `t < 64` and +//! its step 3 says: *"Obtain a bit string of N+t returned_bits from the established DRBG"*. That +//! is a requirement on the per-message secret's draw independent of how close `n` is to a power +//! of two, and 384 bits does not meet it. So `ECDSAP384` now draws `N + 64 = 448` bits (56 +//! bytes, `bouncycastle-ecdsa`'s `keys_p384::EXTRA_BITS_DRBG_OUTPUT_LEN`) and reduces them with this module. +//! Key generation draws the same 56 bytes: Table A.2's 384 is a minimum ("not less than"), so +//! the wider draw is permitted there too, and one reduction path per curve is simpler than two. +//! The extra bits satisfy the requirement rather than lower the bias: for an `n` this close to a +//! power of two, `2^l mod (n-1)` grows with `l` as fast as `2^l` does, so the bias stays near +//! `2^-193` at `l = 448` too (computed, not assumed). +//! +//! The reduction itself is [`crate::barrett`]'s constant-time Barrett reduction with +//! `m = n - 1` (see [`crate::p256_extra_bits`]'s docs for the constant-time argument); `p384_extra_bits_tests.rs` +//! pins known answers computed in Python and cross-checks it against the bit-serial algorithm this +//! module used to implement, kept there as an independent reference. + +use crate::barrett; +use crate::nat; +use crate::p384_scalar::{N_LIMBS, P384Scalar}; + +/// `n - 1`, little-endian `u64` limbs. `N_LIMBS[0]` is odd (`n` is prime), so the subtraction +/// never borrows out of the low limb. +const N_MINUS_1_LIMBS: [u64; 6] = + [N_LIMBS[0] - 1, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3], N_LIMBS[4], N_LIMBS[5]]; + +/// `floor(2^768 / (n-1)) - 2^384`: the low limbs of Barrett's `mu` for `m = n - 1` (whose top +/// limb is `mu`'s only other bit -- see [`crate::barrett`]), computed in Python from +/// SP 800-186 §3.2.1.4's `n`. +const MU_LOW_LIMBS: [u64; 6] = [ + 0x1313e695333ad68e, 0xa7e5f24db74f5885, 0x389cb27e0bc8d220, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, +]; + +/// FIPS 186-5 Appendix A.4.1 steps 3-5 for `n` = the curve order: reduces the big-endian bit string +/// `bytes` modulo `n-1` and adds `1`, landing in `[1, n-1]`. `bytes` may be up to `95` bytes +/// -- `2^760 < (n-1) * 2^384`, Barrett's precondition -- which covers the 56-byte DRBG +/// output `bouncycastle-ecdsa` draws with room to spare; a longer input is a programming error and panics. +pub fn reduce_wide_bits_mod_n_minus_1(bytes: &[u8]) -> P384Scalar { + assert!( + bytes.len() < 96, + "reduce_wide_bits_mod_n_minus_1 given {} bytes, more than 95", + bytes.len() + ); + let t = barrett::limbs_from_be_bytes::<12>(bytes); + let reduced = barrett::reduce::<6, 12, 7>(&t, &N_MINUS_1_LIMBS, &MU_LOW_LIMBS); + let (plus_one, _) = nat::add(&reduced, &[1, 0, 0, 0, 0, 0]); + P384Scalar::from_limbs(plus_one) +} diff --git a/crypto/ec/src/p384_mul.rs b/crypto/ec/src/p384_mul.rs new file mode 100644 index 000000000..014d82896 --- /dev/null +++ b/crypto/ec/src/p384_mul.rs @@ -0,0 +1,97 @@ +//! Constant-time variable-base scalar multiplication: `[k]Q` for a secret `k` and a point `Q` +//! that is not the base point -- a peer's public key, for the ECC CDH primitive of SP 800-56A +//! Rev. 3 Section 5.7.1.2 (`P = h d_A Q_B`). +//! +//! The crate's other two multipliers do not cover this case: [`crate::p384_comb`] is fixed-base +//! (`[k]G` from a compile-time table of multiples of `G`), and [`crate::p384_wnaf`] takes +//! [`crate::p384_scalar::P384PublicScalar`] and is variable-time by design. This one takes +//! [`P384Scalar`], the secret type, and is branch-free in `k` throughout. +//! +//! # Fixed window, width 4 +//! +//! `[k]Q` is computed left to right over `ROUNDS = ceil(BITS / 4)` windows of 4 bits: each round +//! doubles the accumulator four times and adds `[w]Q` for the round's window value `w`, read off +//! `k`'s limbs at positions fixed by the loop (so *which* bits are read is public, only their +//! values are secret). `[w]Q` comes from a table of `[0]Q .. [15]Q` built per call in Jacobian +//! coordinates -- entry 0 is the identity, the rest by doubling and adding `Q` -- and is read by +//! `table_lookup`, which scans every entry under a mask rather than indexing by `w`, as the +//! comb's lookup does. The accumulator's addition is the complete [`P384JacobianPoint::add`]: it +//! masks in the identity, same-point and opposite-point cases, so the accumulator may be the +//! identity (it starts there and stays there through leading zero windows), a table entry may be +//! the identity (`w = 0`), and `Q` itself may be added to a multiple of `Q`; the comb's "no +//! offset" argument carries over unchanged. +//! +//! Cost: `4 * ROUNDS` doublings, `ROUNDS` full additions, `ROUNDS` masked scans of the table and +//! 14 point operations to build it: on P-256 about three times the fixed-base comb (whose table +//! is precomputed and whose additions are mixed) and 1.3 times the variable-time wNAF multiplier +//! (136 us against 44 us and 106 us in `p256_point_benches` / `p256_scalar_benches`). The table +//! is `16 * size_of::()` bytes of stack. Width 4 over width 5 keeps +//! that table at 1.5 KiB here and 3.4 KiB on P-521 for one fewer addition in five; a Montgomery +//! ladder would need an addition per bit. The table and the working accumulator are scrubbed +//! before returning; the caller owns the result and scrubs it when it is done. +//! +//! Verified against [`crate::p384_comb`] (for `Q = G`), against [`crate::p384_wnaf`] and against +//! `[a k mod n]G` (for `Q = [a]G`) in `p384_mul_tests.rs`, including `k = 0`, `k = 1`, `k = n-1`, +//! `Q` the identity, `Q` negated and `Q` in a non-canonical-`Z` representation. + +use crate::p384_point::{P384JacobianPoint, select_point}; +use crate::p384_scalar::P384Scalar; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 4; +const BITS: usize = 384; +const ROUNDS: usize = BITS.div_ceil(WIDTH); +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]Q`, branch-free in `k`. `Q` may be any point, including the identity; `k` may be any +/// value in `[0, n-1]`, and `[0]Q` is the identity. +pub fn multiply_point(k: &P384Scalar, q: &P384JacobianPoint) -> P384JacobianPoint { + let mut table = [P384JacobianPoint::INFINITY; TABLE_SIZE]; + table[1] = *q; + for i in 2..TABLE_SIZE { + // Which entry is built how depends only on the (public) index, never on `k` or `Q`. + table[i] = if i % 2 == 0 { table[i / 2].double() } else { table[i - 1].add(q) }; + } + + let limbs = k.limbs(); + let mut r = P384JacobianPoint::INFINITY; + for round in 0..ROUNDS { + let low_bit = (ROUNDS - 1 - round) * WIDTH; + let window = window_at(limbs, low_bit); + for _ in 0..WIDTH { + r = r.double(); + } + let mut entry = table_lookup(&table, window); + r = r.add(&entry); + entry.zeroize(); + } + + for entry in table.iter_mut() { + entry.zeroize(); + } + r +} + +/// The `WIDTH` bits of `limbs` from bit `low_bit` up, as a window value in `0..TABLE_SIZE`. Bits +/// at or above `BITS` read as zero, so on a curve whose `BITS` is not a multiple of `WIDTH` +/// (P-521) the top window sees only the scalar. The bit positions are public; the shifts and +/// masks are branch-free in the limb values. +fn window_at(limbs: &[u64; 6], low_bit: usize) -> usize { + let mut window = 0usize; + for j in (low_bit..low_bit + WIDTH).rev() { + let bit = if j < BITS { (limbs[j / 64] >> (j % 64)) & 1 } else { 0 }; + window = (window << 1) | (bit as usize); + } + window +} + +/// Table entry `secret_index`, selected by scanning every entry under a mask so the index never +/// steers which memory is read. +fn table_lookup(table: &[P384JacobianPoint; TABLE_SIZE], secret_index: usize) -> P384JacobianPoint { + let mut r = P384JacobianPoint::INFINITY; + for (i, entry) in table.iter().enumerate() { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + r = select_point(is_this_entry, entry, &r); + } + r +} diff --git a/crypto/ec/src/p384_point.rs b/crypto/ec/src/p384_point.rs new file mode 100644 index 000000000..536f0b0d8 --- /dev/null +++ b/crypto/ec/src/p384_point.rs @@ -0,0 +1,357 @@ +//! Branch-free Jacobian-coordinate point arithmetic on the P-384 curve `y^2 = x^3 - 3x + b`. +//! +//! Identical in shape to [`crate::p256_point`] (same `a = -3` formulas, same branch-free +//! exceptional-case handling); see that module's docs for the full derivation and verification +//! methodology, which applies unchanged here (P-384 also has `a = -3` and cofactor `h = 1` per SP +//! 800-186 §3.2.1.4). + +use crate::p384::P384FieldElement; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// A point on the P-384 curve in Jacobian coordinates. +#[derive(Clone, Copy, Debug)] +pub struct P384JacobianPoint { + /// `X`, the Jacobian `X` coordinate. + pub x: P384FieldElement, + /// `Y`, the Jacobian `Y` coordinate. + pub y: P384FieldElement, + /// `Z`, the Jacobian `Z` coordinate; `Z == 0` denotes the point at infinity. + pub z: P384FieldElement, +} + +impl P384JacobianPoint { + /// The point at infinity, the identity of the curve's group. + pub const INFINITY: Self = + Self { x: P384FieldElement::ONE, y: P384FieldElement::ONE, z: P384FieldElement::ZERO }; + + /// Builds a Jacobian point from affine coordinates (`Z = 1`). Does not check that `(x, y)` is + /// actually on the curve -- see [`crate::p384_sec1`] for that. + pub fn from_affine(x: P384FieldElement, y: P384FieldElement) -> Self { + Self { x, y, z: P384FieldElement::ONE } + } + + /// TRUE iff this is the point at infinity. + pub fn is_infinity(&self) -> Condition { + self.z.is_zero() + } + + /// Converts to affine `(x, y)` coordinates, or `None` for the point at infinity. Not constant + /// time: see [`crate::p256_point::P256JacobianPoint::to_affine`]'s docs. + pub fn to_affine(&self) -> Option<(P384FieldElement, P384FieldElement)> { + if self.is_infinity().to_bool() { + return None; + } + let z_inv = self.z.invert(); + let z_inv2 = z_inv.mul(&z_inv); + let z_inv3 = z_inv2.mul(&z_inv); + Some((self.x.mul(&z_inv2), self.y.mul(&z_inv3))) + } + + /// `-self`: negating the `Y` coordinate. + pub fn negate(&self) -> Self { + Self { x: self.x, y: self.y.negate(), z: self.z } + } + + /// Overwrites all three coordinates with zero (which is not the identity's encoding), for + /// scrubbing a secret-derived point before it goes out of scope; see + /// [`P384FieldElement::zeroize`]. + pub fn zeroize(&mut self) { + self.x.zeroize(); + self.y.zeroize(); + self.z.zeroize(); + } + + /// `2 * self`, via the curve's `a = -3` Jacobian doubling shortcut. See + /// [`crate::p256_point::P256JacobianPoint::double`]'s docs. + pub fn double(&self) -> Self { + let y1_squared = self.y.mul(&self.y); + let t = y1_squared.mul(&y1_squared); + let z1_squared = self.z.mul(&self.z); + let t1 = self.x.sub(&z1_squared); + let m = self.x.add(&z1_squared); + let m = m.mul(&t1); + let m = m.add(&m).add(&m); + let s = y1_squared.mul(&self.x); + let s = s.add(&s).add(&s).add(&s); + let t8 = t.add(&t).add(&t).add(&t).add(&t).add(&t).add(&t).add(&t); + let x3 = m.mul(&m).sub(&s).sub(&s); + let y3 = s.sub(&x3); + let y3 = y3.mul(&m).sub(&t8); + let z3 = self.y.add(&self.y).mul(&self.z); + Self { x: x3, y: y3, z: z3 } + } + + /// The generic (`self != ±other`, neither infinite) Jacobian addition formula, plus the `H` + /// and `R` intermediates [`Self::add`] uses to detect the exceptional cases. See + /// [`crate::p256_point::P256JacobianPoint`]'s private `generic_add`'s docs. + fn generic_add(&self, other: &Self) -> (Self, P384FieldElement, P384FieldElement) { + let z2_sq = other.z.mul(&other.z); + let u1 = self.x.mul(&z2_sq); + let s1 = self.y.mul(&other.z).mul(&z2_sq); + let z1_sq = self.z.mul(&self.z); + let u2 = other.x.mul(&z1_sq); + let s2 = other.y.mul(&self.z).mul(&z1_sq); + + let h = u1.sub(&u2); + let r = s1.sub(&s2); + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); + let v = h_squared.mul(&u1); + let g_neg = g.negate(); + let acc = s1.mul(&g_neg); + let g2 = g_neg.add(&v).add(&v); + let x3 = r.mul(&r).sub(&g2); + let y3_temp = v.sub(&x3); + let y3 = acc.add(&y3_temp.mul(&r)); + let z3 = h.mul(&self.z).mul(&other.z); + + (Self { x: x3, y: y3, z: z3 }, h, r) + } + + /// `self + other`, branch-free per [`crate::p256_point::P256JacobianPoint::add`]'s docs. + pub fn add(&self, other: &Self) -> Self { + let self_is_infinity = self.is_infinity(); + let other_is_infinity = other.is_infinity(); + + let (generic, h, r) = self.generic_add(other); + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for an `other` given in **affine** coordinates (`Z2 = 1`), or the identity + /// when `other_is_infinity` is TRUE (`x2`/`y2` are then ignored). The same group law and the + /// same branch-free discipline as [`Self::add`] -- generic-add and doubling candidates computed + /// unconditionally, the exceptional cases masked in -- but with `Z2 = 1` the generic formula + /// simplifies: `U1 = X1`, `S1 = Y1` and `Z3 = H * Z1`, which drops five of its sixteen field + /// multiplications. The fixed-base comb multiplier adds a table entry to its accumulator on + /// every round, and every table entry is affine or the identity, so it is the caller this + /// exists for; `pub(crate)` because a caller has to know its point really has `Z = 1`. + pub(crate) fn add_affine( + &self, + x2: &P384FieldElement, + y2: &P384FieldElement, + other_is_infinity: Condition, + ) -> Self { + let self_is_infinity = self.is_infinity(); + + // The generic mixed addition, computed unconditionally: `U1 = X1`, `S1 = Y1`. + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2 + let r = self.y.sub(&s2); // S1 - S2 + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); // R^2 + H^3 - 2V + let y3 = acc.add(&v.sub(&x3).mul(&r)); // -S1*H^3 + (V - X3)*R + let z3 = h.mul(&self.z); // H * Z1 * Z2 with Z2 = 1 + let generic = Self { x: x3, y: y3, z: z3 }; + + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + let other = Self { x: *x2, y: *y2, z: P384FieldElement::ONE }; + + // Same priority order as `add`: the identity cases override everything else. + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, &other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for points that are **public**, branching on the exceptional cases instead + /// of computing every candidate and masking. + /// + /// [`Self::add`] must evaluate the doubling candidate on every call, because on secret inputs + /// it cannot branch on whether the doubling case applies -- that costs a full point doubling + /// (roughly a third of the addition) on every addition, whether or not it is ever used. This + /// version pays it only when the operands really are the same point, which for a scalar + /// multiplier over distinct precomputed multiples is essentially never. + /// + /// Restricted to `pub(crate)` and used only by [`crate::p384_wnaf`], whose scalars are + /// [`crate::p384_scalar::P384PublicScalar`] and whose points are a signer's public key and + /// the curve's own base point: every value it branches on is already known to an attacker. Do + /// not call this on anything derived from a private key or a per-message secret -- the + /// constant-time [`Self::add`] exists for that, and the scalar type split is what keeps the + /// two multipliers from being confused for one another. + pub(crate) fn add_vartime(&self, other: &Self) -> Self { + if self.is_infinity().to_bool() { + return *other; + } + if other.is_infinity().to_bool() { + return *self; + } + + let (generic, h, r) = self.generic_add(other); + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + generic + } + /// `self + (x2, y2)` for a **public**, affine `other` (`Z2 = 1`): [`Self::add_vartime`]'s + /// early-exit structure over [`Self::add_affine`]'s mixed formula, for the variable-time + /// multiplier's fixed side, whose precomputed multiples of `G` are stored affine. + /// `pub(crate)` for both of the reasons its two parents are. + pub(crate) fn add_vartime_affine(&self, x2: &P384FieldElement, y2: &P384FieldElement) -> Self { + if self.is_infinity().to_bool() { + return Self::from_affine(*x2, *y2); + } + + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2, with U1 = X1 + let r = self.y.sub(&s2); // S1 - S2, with S1 = Y1 + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); + let y3 = acc.add(&v.sub(&x3).mul(&r)); + let z3 = h.mul(&self.z); // H * Z1, with Z2 = 1 + Self { x: x3, y: y3, z: z3 } + } +} + +pub(crate) fn select_point( + cond: Condition, + a: &P384JacobianPoint, + b: &P384JacobianPoint, +) -> P384JacobianPoint { + P384JacobianPoint { + x: P384FieldElement::from_internal_limbs(select_limbs( + cond, + &a.x.internal_limbs(), + &b.x.internal_limbs(), + )), + y: P384FieldElement::from_internal_limbs(select_limbs( + cond, + &a.y.internal_limbs(), + &b.y.internal_limbs(), + )), + z: P384FieldElement::from_internal_limbs(select_limbs( + cond, + &a.z.internal_limbs(), + &b.z.internal_limbs(), + )), + } +} + +fn select_limbs(cond: Condition, a: &[u64; 6], b: &[u64; 6]) -> [u64; 6] { + let mut out = [0u64; 6]; + ct::conditional_select(cond, a, b, &mut out); + out +} + +// `add_vartime` is `pub(crate)` -- deliberately unreachable from outside the crate, since calling +// it on a secret point would undo the constant-time discipline [`P384JacobianPoint::add`] exists +// for -- so no integration test can reach it. Its whole contract is that it computes the same group +// law as `add`, differing only in *how* it gets there, so that is what is pinned here: agreement on +// every exceptional case, including the ones a scalar multiplier over distinct precomputed +// multiples essentially never reaches by chance (a point added to itself, and a point added to its +// own negation). QUALITY_AND_STYLE's private-function carve-out applies. +#[cfg(test)] +mod tests { + use super::*; + use crate::p384_domain::{G_X_LIMBS, G_Y_LIMBS}; + + #[test] + fn add_affine_agrees_with_add_on_every_case() { + let g = P384JacobianPoint::from_affine( + P384FieldElement::from_limbs(G_X_LIMBS), + P384FieldElement::from_limbs(G_Y_LIMBS), + ); + // A non-canonical-Z representation of G as well: `add_affine` reads `self.z`, so `self` + // must not be assumed affine even though `other` is. + let z = P384FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = P384JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + let selves = [g, g.double(), g.negate(), P384JacobianPoint::INFINITY, scaled_g]; + let others = [g, g.double(), g.negate()]; + for a in selves { + for b in others { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_affine(&bx, &by, Condition::::FALSE).to_affine(), + a.add(&b).to_affine(), + "add_affine disagrees with add" + ); + } + // The identity flag must win regardless of the coordinates passed alongside it. + assert_eq!( + a.add_affine(&g.x, &g.y, Condition::::TRUE).to_affine(), + a.to_affine(), + "add_affine with the identity flag must return self" + ); + } + } + + #[test] + fn add_vartime_affine_agrees_with_add_on_every_case() { + let g = P384JacobianPoint::from_affine( + P384FieldElement::from_limbs(G_X_LIMBS), + P384FieldElement::from_limbs(G_Y_LIMBS), + ); + let z = P384FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = P384JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + for a in [g, g.double(), g.negate(), P384JacobianPoint::INFINITY, scaled_g] { + for b in [g, g.double(), g.negate()] { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_vartime_affine(&bx, &by).to_affine(), + a.add(&b).to_affine(), + "add_vartime_affine disagrees with add" + ); + } + } + } + + #[test] + fn add_vartime_agrees_with_add_on_every_case() { + let g = P384JacobianPoint::from_affine( + P384FieldElement::from_limbs(G_X_LIMBS), + P384FieldElement::from_limbs(G_Y_LIMBS), + ); + // G, 2G, -G and the identity cover all four of `add`'s exceptional cases pairwise, plus + // the ordinary one (e.g. G + 2G). + let points = [g, g.double(), g.negate(), P384JacobianPoint::INFINITY]; + for a in points { + for b in points { + assert_eq!( + a.add_vartime(&b).to_affine(), + a.add(&b).to_affine(), + "add_vartime disagrees with add" + ); + } + } + } +} diff --git a/crypto/ec/src/p384_scalar.rs b/crypto/ec/src/p384_scalar.rs new file mode 100644 index 000000000..e145a6053 --- /dev/null +++ b/crypto/ec/src/p384_scalar.rs @@ -0,0 +1,414 @@ +//! The P-384 scalar field, arithmetic mod the curve order `n` (SP 800-186 §3.2.1.4). Identical in +//! shape to [`crate::p256_scalar`] -- Montgomery form (`R = 2^384`), the same SOS reduction, the +//! same `Secret`/plain scalar split -- with `n` swapped for P-384's; see that module's docs for +//! the full derivation. +//! +//! REDC's accumulator, like [`crate::p256_scalar::redc`]'s, needs one bit of headroom beyond the +//! 12-limb product (`n` is close enough to `2^384` that `2n` can exceed it): verified (not checked +//! in) against 30,000 random trials plus the `(n-1)*(n-1)` worst case. + +use crate::nat; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +/// `n`, the order of the P-384 base point `G`, little-endian `u64` limbs (SP 800-186 §3.2.1.4). +pub const N_LIMBS: [u64; 6] = [ + 0xecec196accc52973, 0x581a0db248b0a77a, 0xc7634d81f4372ddf, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, +]; + +/// `2^384 mod n`, also the Montgomery representation of `1`. +const R_LIMBS: [u64; 6] = [ + 0x1313e695333ad68d, 0xa7e5f24db74f5885, 0x389cb27e0bc8d220, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, +]; + +/// `R^2 mod n`, used to bring a plain value into Montgomery form. +const R_SQUARED_LIMBS: [u64; 6] = [ + 0x2d319b2419b409a9, 0xff3d81e5df1aa419, 0xbc3e483afcb82947, 0xd40d49174aab1cc5, + 0x3fb05b7a28266895, 0x0c84ee012b39bf21, +]; + +/// `-n^-1 mod 2^64`, the Montgomery reduction constant. +const N_PRIME: u64 = 0x6ed46089e88fdc45; + +/// `n - 2`, little-endian `u64` limbs -- the public exponent [`P384ScalarField::invert`] raises +/// its base to. +const N_MINUS_2_LIMBS: [u64; 6] = [ + 0xecec196accc52971, 0x581a0db248b0a77a, 0xc7634d81f4372ddf, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, +]; + +/// An element of the P-384 scalar field, i.e. an integer mod `n`. Stored internally in Montgomery +/// form; see the module docs. +#[derive(Clone, Copy, Debug)] +pub struct P384ScalarField([u64; 6]); + +impl P384ScalarField { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0, 0, 0]); + + /// The multiplicative identity. Its Montgomery form is `R mod n`, not the literal integer `1`. + pub const ONE: Self = Self(R_LIMBS); + + /// Builds a scalar-field element from little-endian `u64` limbs (an ordinary, non-Montgomery + /// value), reducing once if `limbs >= n`, then converting to Montgomery form. + pub fn from_limbs(limbs: [u64; 6]) -> Self { + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + Self(redc(&widening_mul(&reduced, &R_SQUARED_LIMBS))) + } + + /// Returns the canonical little-endian `u64` limbs (an ordinary, non-Montgomery value), `< n`. + pub fn to_limbs(&self) -> [u64; 6] { + let mut wide = [0u64; 12]; + wide[..6].copy_from_slice(&self.0); + redc(&wide) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + nat::is_zero(&self.0) + } + + /// `self + other mod n`. Operates directly on the Montgomery-form limbs: no REDC needed. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = nat::add(&self.0, &other.0); + let (sum_plus_r, _) = nat::add(&sum, &R_LIMBS); + let (diff, borrow) = nat::sub(&sum, &N_LIMBS); + let mut when_no_carry = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 6]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_r, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod n`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = nat::sub(&self.0, &other.0); + let (corrected, _) = nat::add(&diff, &N_LIMBS); + let mut result = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod n`. + pub fn negate(&self) -> Self { + let (diff, _) = nat::sub(&N_LIMBS, &self.0); + let mut result = [0u64; 6]; + ct::conditional_select(self.is_zero(), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod n`, via Montgomery multiplication. + pub fn mul(&self, other: &Self) -> Self { + Self(redc(&widening_mul(&self.0, &other.0))) + } + + /// Overwrites this value with zero, through a write the compiler may not elide. + /// + /// For the signing path, where a value of this type holds `d`, `k` or `k^-1` and would + /// otherwise stay legible on the stack after the signature is returned. This type is `Copy` + /// and carries public values too (verification's `u`, `v`, `e`, `r`, `s`), so it is not + /// wrapped in [`bouncycastle_utils::secret::Secret`] -- that would cost its `Copy`, its `const + /// ZERO`/`ONE`, and a scrub on every intermediate including the public ones. The trade is that + /// scrubbing is the caller's job, on every path out of the function; see + /// `bouncycastle_ecdsa`'s `sign_with_k` for the intended use. + /// + /// This does not reach values the compiler kept in registers or spilled itself, and is + /// defence in depth rather than a guarantee. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`widening_square`]. + /// + /// Used by the exponentiation loops ([`Self::invert`] and friends), which call it hundreds of + /// times in a row with nothing else competing for registers. Deliberately *not* used by this + /// curve's point arithmetic, which squares by calling `mul` with equal arguments: that was + /// tried and measured on P-256, and despite `square` being ~9% cheaper than `mul` in + /// isolation it made the constant-time comb multiplier consistently slower, because inlining + /// a third wide-multiply routine into that loop costs more than the saved partial products + /// return. See [`crate::p256::P256FieldElement::square`] for the numbers. + pub fn square(&self) -> Self { + Self(redc(&widening_square(&self.0))) + } + + /// Converts a secret scalar (`d` or `k`) into Montgomery form for field arithmetic. See + /// [`crate::p256_scalar::P256ScalarField::from_secret`]'s docs. + pub fn from_secret(secret: &P384Scalar) -> Self { + Self::from_limbs(*secret.limbs()) + } + + /// `self^-1 mod n`, or `0` if `self` is `0`. Fermat's little theorem (`n` is prime) over the + /// public exponent `n-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..6).rev() { + let limb = N_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } +} + +impl PartialEq for P384ScalarField { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. Comparing Montgomery-form limbs directly is valid: the map `x -> x*R mod n` is a + /// bijection. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..6 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for P384ScalarField {} + +/// Schoolbook widening multiply, identical in shape to [`crate::p256_scalar`]'s. +fn widening_mul(a: &[u64; 6], b: &[u64; 6]) -> [u64; 12] { + let mut result = [0u64; 12]; + for i in 0..6 { + let mut carry: u128 = 0; + for j in 0..6 { + let idx = i + j; + let prod = (a[i] as u128) * (b[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + let mut k = i + 6; + while k < 12 { + let s = (result[k] as u128) + carry; + result[k] = s as u64; + carry = s >> 64; + k += 1; + } + } + result +} + +/// Schoolbook squaring of a 6-limb value into its 12-limb square. +/// +/// `a * a` is symmetric: the product `a_i * a_j` appears twice for every `i != j`. Forming each of +/// those once and doubling costs `L(L+1)/2` limb multiplications -- 21 at this width against +/// the 36 [`widening_mul`] would form for the same value. +/// +/// Three passes: the off-diagonal products `a_i * a_j` for `i < j`; a doubling of the whole +/// accumulator; then the diagonal squares `a_i * a_i` added in at limb `2i`. Writing `result[i + +/// 6]` in the first pass is an assignment rather than an accumulation because row `i` only ever +/// reaches limbs `2i + 1 ..= i + 6 - 1`, and no earlier row reaches limb `i + 6` either, so that +/// limb is still zero when the row's final carry lands on it. The doubling cannot overflow: the +/// off-diagonal sum is strictly less than `a^2 / 2`. +/// +/// Verified against Python's arbitrary-precision `**2` over 20,000 random 6-limb values plus the +/// all-ones worst case before being written here. +fn widening_square(a: &[u64; 6]) -> [u64; 12] { + let mut result = [0u64; 12]; + + // Off-diagonal products, each formed once. + for i in 0..6 { + let mut carry: u128 = 0; + for j in (i + 1)..6 { + let idx = i + j; + let prod = (a[i] as u128) * (a[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + result[i + 6] = carry as u64; + } + + // Every off-diagonal product appears twice in the square. + let mut carry = 0u64; + for limb in result.iter_mut() { + let next_carry = *limb >> 63; + // `*limb << 1` always leaves bit 0 clear and `carry` is only ever 0 or 1, so the two + // operands are disjoint: mutating this `|` to `^` is an accepted equivalent, not a bug. + *limb = (*limb << 1) | carry; + carry = next_carry; + } + debug_assert_eq!(carry, 0, "doubling the off-diagonal sum overflowed 12 limbs"); + + // Diagonal squares. + let mut carry: u128 = 0; + for i in 0..6 { + let square = (a[i] as u128) * (a[i] as u128); + let low = (result[2 * i] as u128) + ((square as u64) as u128) + carry; + result[2 * i] = low as u64; + let high = (result[2 * i + 1] as u128) + (square >> 64) + (low >> 64); + result[2 * i + 1] = high as u64; + carry = high >> 64; + } + debug_assert_eq!(carry, 0, "adding the diagonal overflowed 12 limbs"); + + result +} + +/// `m * n`, one limb times the 6-limb modulus `n`, as the 7 limbs such a product can occupy. +/// +/// This is exactly what each round of [`redc`]'s SOS reduction needs, and the shape +/// [`crate::montgomery::redc`] has always used for the brainpool curves. Every hand-written scalar +/// REDC in this crate previously formed it with the general 6x6 [`widening_mul`] against a +/// zero-padded `[m, 0, ..., 0]` instead, asking for 36 limb multiplications where 6 are needed. +/// +/// Whether that padding costs anything turns out to depend entirely on the width, measured rather +/// than assumed: at four limbs the optimizer eliminates it completely (P-256 and secp256k1 time +/// identically either way), but it stops doing so by six, where the padded form measured 3.2x +/// slower per scalar multiplication for P-384 and 5.2x slower for P-521. All four are written this +/// way regardless, so the four modules read alike and none of them asks for work it does not want. +fn modulus_times_limb(m: u64) -> [u64; 7] { + let mut out = [0u64; 7]; + let mut carry: u128 = 0; + for j in 0..6 { + let prod = (m as u128) * (N_LIMBS[j] as u128) + carry; + out[j] = prod as u64; + carry = prod >> 64; + } + out[6] = carry as u64; + out +} + +/// Montgomery reduction (SOS method): given a 12-limb value `t < n*R`, returns `t * R^-1 mod n` as +/// 6 limbs. See [`crate::p256_scalar::redc`]'s docs for the algorithm; the 13th accumulator limb +/// plays the same role as that function's 9th. +fn redc(t: &[u64; 12]) -> [u64; 6] { + let mut acc = [0u64; 13]; + acc[..12].copy_from_slice(t); + + for i in 0..6 { + let m = acc[i].wrapping_mul(N_PRIME); + let mn = modulus_times_limb(m); + + let mut carry: u128 = 0; + for j in 0..7 { + let idx = i + j; + let s = (acc[idx] as u128) + (mn[j] as u128) + carry; + acc[idx] = s as u64; + carry = s >> 64; + } + // Propagate the carry through every limb above the window, up to the accumulator's + // proven 13-limb bound. The trip count depends only on the public round index `i`, + // never on the operands: once the carry has propagated out, the remaining iterations + // add 0. A `while carry != 0` here would be a secret-dependent branch on the signing + // path. + for idx in (i + 7)..13 { + let s = (acc[idx] as u128) + carry; + acc[idx] = s as u64; + carry = s >> 64; + } + debug_assert_eq!(carry, 0, "carry must be fully propagated within the 13-limb bound"); + } + debug_assert_eq!(&acc[..6], &[0u64; 6], "REDC's low limbs must be cleared after 6 rounds"); + debug_assert!(acc[12] == 0 || acc[12] == 1, "REDC's top limb must be a single bit"); + + let high: [u64; 6] = [acc[6], acc[7], acc[8], acc[9], acc[10], acc[11]]; + let (high_plus_r, _) = nat::add(&high, &R_LIMBS); + let (diff, borrow) = nat::sub(&high, &N_LIMBS); + let mut when_no_extra_bit = [0u64; 6]; + ct::conditional_select( + Condition::::from_lsb(borrow), + &high, + &diff, + &mut when_no_extra_bit, + ); + let mut result = [0u64; 6]; + ct::conditional_select( + Condition::::from_lsb(acc[12]), + &high_plus_r, + &when_no_extra_bit, + &mut result, + ); + result +} + +fn reduce_once(limbs: [u64; 6]) -> [u64; 6] { + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut result = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut result); + result +} + +/// A scalar mod `n`, `< n`, held in `Secret`. See [`crate::p256_scalar::P256Scalar`]'s docs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct P384Scalar(Secret<[u64; 6]>); + +impl P384Scalar { + /// Builds a secret scalar from little-endian `u64` limbs, reducing once if `limbs >= n`. + pub fn from_limbs(limbs: [u64; 6]) -> Self { + let mut secret = Secret::<[u64; 6]>::new(); + *secret = reduce_once(limbs); + Self(secret) + } + + /// Builds a secret scalar from SEC 1 §2.3.7 big-endian octets, reducing once if `>= n`. + pub fn from_be_bytes(bytes: &[u8; 48]) -> Self { + Self::from_limbs(crate::p384_sec1::limbs_from_be_bytes(bytes)) + } + + /// Encodes to SEC 1 §2.3.7 big-endian octets. + pub fn to_be_bytes(&self) -> [u8; 48] { + crate::p384_sec1::be_bytes_from_limbs(&self.0) + } + + /// The scalar's limbs, for the crate's own multiplier and field-arithmetic implementations to + /// read. Not exposed outside the crate. + pub(crate) fn limbs(&self) -> &[u64; 6] { + &self.0 + } +} + +/// A scalar mod `n`, `< n`, held as a plain (non-secret) value. See +/// [`crate::p256_scalar::P256PublicScalar`]'s docs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct P384PublicScalar([u64; 6]); + +impl P384PublicScalar { + /// Builds a public scalar from little-endian `u64` limbs, reducing once if `limbs >= n`. + pub fn from_limbs(limbs: [u64; 6]) -> Self { + Self(reduce_once(limbs)) + } + + /// Returns the scalar's little-endian `u64` limbs, `< n`. + pub fn to_limbs(&self) -> [u64; 6] { + self.0 + } + + /// `self^-1 mod n` in **variable time**, as a [`P384ScalarField`] ready for the arithmetic + /// that follows, or `0` for `0`. For ECDSA verification's `s` (FIPS 186-5 §6.4.2 step 4), + /// which is part of the signature and so already public; nothing derived from a private key + /// or a per-message secret may reach this, which is why it lives on the public scalar type + /// and not on [`P384ScalarField`], whose `invert` is the constant-time one. See + /// [`crate::inverse_vartime`]. + pub fn invert_vartime(&self) -> P384ScalarField { + P384ScalarField::from_limbs(crate::inverse_vartime::mod_inverse(&self.0, &N_LIMBS)) + } +} diff --git a/crypto/ec/src/p384_sec1.rs b/crypto/ec/src/p384_sec1.rs new file mode 100644 index 000000000..6b24b1890 --- /dev/null +++ b/crypto/ec/src/p384_sec1.rs @@ -0,0 +1,181 @@ +//! SEC 1 v2.0 (Certicom, May 2009) §2.3.3/§2.3.4 point encoding and decoding for P-384, and the +//! public-key validation SP 800-186 (Feb 2023) Appendix D.1.1.1 requires. Identical in shape to +//! [`crate::p256_sec1`] -- see that module's docs for the full reasoning (SEC 1 §2.3.3/§2.3.4 +//! cases, why there is no point-at-infinity case, why partial validation is full validation here, +//! and the compressed-decode square-root derivation) -- with the width and curve constants +//! swapped for P-384's (also `p = 3 mod 4` and cofactor `h = 1`, verified the same way in this +//! module's tests). + +use crate::nat; +use crate::p384::{P_LIMBS, P384FieldElement}; +use crate::p384_domain::B_LIMBS; +use crate::p384_point::P384JacobianPoint; + +/// `(p + 1) / 4`, the exponent [`decode`]'s compressed-point square root raises `α` to. +const SQRT_EXPONENT: [u64; 6] = [ + 0x0000000040000000, 0xbfffffffc0000000, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0x3fffffffffffffff, +]; + +/// Converts 48 big-endian bytes to little-endian `u64` limbs. +pub fn limbs_from_be_bytes(bytes: &[u8; 48]) -> [u64; 6] { + let mut limbs = [0u64; 6]; + for i in 0..6 { + limbs[i] = u64::from_be_bytes(bytes[(40 - i * 8)..(48 - i * 8)].try_into().unwrap()); + } + limbs +} + +/// Converts little-endian `u64` limbs to 48 big-endian bytes. +pub fn be_bytes_from_limbs(limbs: &[u64; 6]) -> [u8; 48] { + let mut out = [0u8; 48]; + for i in 0..6 { + out[(40 - i * 8)..(48 - i * 8)].copy_from_slice(&limbs[i].to_be_bytes()); + } + out +} + +fn limbs_less_than_p(limbs: &[u64; 6]) -> bool { + let (_, borrow) = nat::sub(limbs, &P_LIMBS); + borrow == 1 +} + +fn either_out_of_range(x_limbs: &[u64; 6], y_limbs: &[u64; 6]) -> bool { + !limbs_less_than_p(x_limbs) || !limbs_less_than_p(y_limbs) +} + +/// `y^2 == x^3 - 3x + b`. +fn is_on_curve(x: &P384FieldElement, y: &P384FieldElement) -> bool { + let lhs = y.mul(y); + let x_cubed = x.mul(x).mul(x); + let three_x = x.add(x).add(x); + let rhs = x_cubed.sub(&three_x).add(&P384FieldElement::from_limbs(B_LIMBS)); + lhs == rhs +} + +/// `base^SQRT_EXPONENT mod p`. Plain (not masked) square-and-multiply: only ever runs on public +/// data (an incoming encoded point), like [`crate::p256_sec1::pow_public`]. +fn pow_public(base: &P384FieldElement, exponent_limbs: &[u64; 6]) -> P384FieldElement { + let mut result = P384FieldElement::ONE; + for limb_idx in (0..6).rev() { + let limb = exponent_limbs[limb_idx]; + for bit in (0..64).rev() { + result = result.square(); + if (limb >> bit) & 1 == 1 { + result = result.mul(base); + } + } + } + result +} + +/// SEC 1 §2.3.3 case 3: `04 || X || Y`, 97 bytes. +pub fn encode_uncompressed(x: &P384FieldElement, y: &P384FieldElement) -> [u8; 97] { + let mut out = [0u8; 97]; + out[0] = 0x04; + out[1..49].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out[49..97].copy_from_slice(&be_bytes_from_limbs(&y.to_limbs())); + out +} + +/// SEC 1 §2.3.3 case 2: `02 || X` or `03 || X`, 49 bytes, tag selected by `y mod 2`. +pub fn encode_compressed(x: &P384FieldElement, y: &P384FieldElement) -> [u8; 49] { + let mut out = [0u8; 49]; + out[0] = if y.to_limbs()[0] & 1 == 0 { 0x02 } else { 0x03 }; + out[1..49].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out +} + +/// Decodes and validates a SEC 1 §2.3.4 point encoding: 49 bytes (compressed) or 97 bytes +/// (uncompressed). See [`crate::p256_sec1::decode`]'s docs for exactly which checks this performs. +pub fn decode(bytes: &[u8]) -> Option<(P384FieldElement, P384FieldElement)> { + match bytes.len() { + 49 => { + let y_tilde: u64 = match bytes[0] { + 0x02 => 0, + 0x03 => 1, + _ => return None, + }; + let x_bytes: [u8; 48] = bytes[1..49].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + if !limbs_less_than_p(&x_limbs) { + return None; + } + let x = P384FieldElement::from_limbs(x_limbs); + let x_cubed = x.mul(&x).mul(&x); + let three_x = x.add(&x).add(&x); + let alpha = x_cubed.sub(&three_x).add(&P384FieldElement::from_limbs(B_LIMBS)); + let beta = pow_public(&alpha, &SQRT_EXPONENT); + if beta.mul(&beta) != alpha { + return None; + } + let beta_parity = beta.to_limbs()[0] & 1; + let y = if beta_parity == y_tilde { beta } else { beta.negate() }; + Some((x, y)) + } + 97 => { + if bytes[0] != 0x04 { + return None; + } + let x_bytes: [u8; 48] = bytes[1..49].try_into().ok()?; + let y_bytes: [u8; 48] = bytes[49..97].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + let y_limbs = limbs_from_be_bytes(&y_bytes); + if either_out_of_range(&x_limbs, &y_limbs) { + return None; + } + let x = P384FieldElement::from_limbs(x_limbs); + let y = P384FieldElement::from_limbs(y_limbs); + if !is_on_curve(&x, &y) { + return None; + } + Some((x, y)) + } + _ => None, + } +} + +/// Decodes and validates an encoded public key (see [`decode`]) directly into a +/// [`P384JacobianPoint`]. +pub fn decode_point(bytes: &[u8]) -> Option { + let (x, y) = decode(bytes)?; + Some(P384JacobianPoint::from_affine(x, y)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn p_is_3_mod_4() { + assert_eq!(P_LIMBS[0] & 0b11, 3); + } + + #[test] + fn limbs_less_than_p_boundary_cases() { + assert!(limbs_less_than_p(&[0, 0, 0, 0, 0, 0])); + + let mut p_minus_1 = P_LIMBS; + p_minus_1[0] -= 1; + assert!(limbs_less_than_p(&p_minus_1)); + + assert!(!limbs_less_than_p(&P_LIMBS)); + + let mut p_plus_2_pow_32 = P_LIMBS; + p_plus_2_pow_32[0] += 1 << 32; // limb 0's top 32 bits are 0, so this cannot overflow + assert!(!limbs_less_than_p(&p_plus_2_pow_32)); + + assert!(!limbs_less_than_p(&[u64::MAX; 6])); + } + + #[test] + fn either_out_of_range_requires_only_one_coordinate_to_be_out_of_range() { + let in_range = [0u64; 6]; + let out_of_range = P_LIMBS; + + assert!(!either_out_of_range(&in_range, &in_range)); + assert!(either_out_of_range(&out_of_range, &in_range)); + assert!(either_out_of_range(&in_range, &out_of_range)); + assert!(either_out_of_range(&out_of_range, &out_of_range)); + } +} diff --git a/crypto/ec/src/p384_wnaf.rs b/crypto/ec/src/p384_wnaf.rs new file mode 100644 index 000000000..1bba8b44b --- /dev/null +++ b/crypto/ec/src/p384_wnaf.rs @@ -0,0 +1,130 @@ +//! Interleaved windowed-NAF (wNAF) Shamir's-trick multiplication: `[u]G + [v]Q`, for P-384 ECDSA +//! verification. Identical algorithm to [`crate::p256_wnaf`] -- see that module's docs for the +//! full derivation and verification methodology -- with the width and limb count swapped for +//! P-384's (`WNAF_LEN = 385`, one more than `n`'s 384-bit length, same margin as P-256's `257 = +//! 256 + 1`). +//! +//! `G`'s odd multiples are a compile-time affine table (`p384_wnaf_table`, width 7) added by mixed +//! addition; only `Q`'s table is built per call. See [`crate::p256_wnaf`]. + +use crate::nat; +use crate::p384::P384FieldElement; +use crate::p384_point::P384JacobianPoint; +use crate::p384_scalar::P384PublicScalar; +use crate::p384_wnaf_table::{G_ODD_MULTIPLES_X, G_ODD_MULTIPLES_Y}; + +/// Window for `Q`, whose odd multiples are computed per call: 8 point additions to build the +/// table, one addition per ~6 digits to use it. +const WIDTH: usize = 5; +/// Window for `G`, whose odd multiples are a compile-time table +/// ([`G_ODD_MULTIPLES_X`]/[`G_ODD_MULTIPLES_Y`]): nothing to build, so the widest window a +/// signed `i8` digit allows, giving one addition per ~8 digits instead of ~6, and every one of +/// them a mixed addition against an affine entry. +const G_WIDTH: usize = 7; +const WNAF_LEN: usize = 385; +const ODD_MULTIPLE_COUNT: usize = 1 << (WIDTH - 2); + +/// `[u]G + [v]Q`. +pub fn shamir_multiply( + u: &P384PublicScalar, + v: &P384PublicScalar, + q: &P384JacobianPoint, +) -> P384JacobianPoint { + let du = compute_wnaf(u.to_limbs(), G_WIDTH); + let dv = compute_wnaf(v.to_limbs(), WIDTH); + + let table_q = odd_multiples(q); + + let mut r = P384JacobianPoint::INFINITY; + for i in (0..WNAF_LEN).rev() { + r = r.double(); + if let Some((x, y)) = lookup_signed_g(du[i]) { + r = r.add_vartime_affine(&x, &y); + } + if let Some(add_q) = lookup_signed(&table_q, dv[i]) { + r = r.add_vartime(&add_q); + } + } + r +} + +fn shr1(limbs: &[u64; 6]) -> [u64; 6] { + [ + (limbs[0] >> 1) | ((limbs[1] & 1) << 63), + (limbs[1] >> 1) | ((limbs[2] & 1) << 63), + (limbs[2] >> 1) | ((limbs[3] & 1) << 63), + (limbs[3] >> 1) | ((limbs[4] & 1) << 63), + (limbs[4] >> 1) | ((limbs[5] & 1) << 63), + limbs[5] >> 1, + ] +} + +/// The width-`width` wNAF digits of `k` (`width` at most 7, so a digit fits an `i8`), LSB-first; unused trailing entries are `0`. Not +/// constant time: only ever called on public scalars (see the module docs). +fn compute_wnaf(k_limbs: [u64; 6], width: usize) -> [i8; WNAF_LEN] { + let mut digits = [0i8; WNAF_LEN]; + let mut k = k_limbs; + let mut pos = 0; + while k != [0, 0, 0, 0, 0, 0] { + if k[0] & 1 == 1 { + let mut digit = (k[0] & ((1 << width) - 1)) as i16; + if digit >= 1 << (width - 1) { + digit -= 1 << width; + } + k = if digit >= 0 { + let (new_k, borrow) = nat::sub(&k, &[digit as u64, 0, 0, 0, 0, 0]); + debug_assert_eq!(borrow, 0); + new_k + } else { + let (new_k, carry) = nat::add(&k, &[(-digit) as u64, 0, 0, 0, 0, 0]); + debug_assert_eq!(carry, 0); + new_k + }; + debug_assert!(pos < WNAF_LEN, "wNAF encoding overflowed its verified bound"); + digits[pos] = digit as i8; + } + k = shr1(&k); + pos += 1; + } + digits +} + +/// The odd multiples `1*p, 3*p, 5*p, ..., (2*ODD_MULTIPLE_COUNT - 1)*p`, indexed so entry `i` +/// holds `(2i+1)*p`. +fn odd_multiples(p: &P384JacobianPoint) -> [P384JacobianPoint; ODD_MULTIPLE_COUNT] { + let double_p = p.double(); + let mut table = [*p; ODD_MULTIPLE_COUNT]; + let mut cur = *p; + for entry in table.iter_mut() { + *entry = cur; + cur = cur.add_vartime(&double_p); + } + table +} + +/// `digit * p` where `p` is the point `table` was built from ([`odd_multiples`]), or `None` for +/// digit `0`. +fn lookup_signed( + table: &[P384JacobianPoint; ODD_MULTIPLE_COUNT], + digit: i8, +) -> Option { + if digit == 0 { + None + } else if digit > 0 { + Some(table[(digit as usize - 1) / 2]) + } else { + Some(table[(-digit as usize - 1) / 2].negate()) + } +} + +/// `digit * G` for a width-[`G_WIDTH`] digit, read from the compile-time table of odd multiples +/// of `G` as affine coordinates (negating `y` for a negative digit), or `None` for digit `0`. +fn lookup_signed_g(digit: i8) -> Option<(P384FieldElement, P384FieldElement)> { + if digit == 0 { + return None; + } + let index = (digit.unsigned_abs() as usize - 1) / 2; + let x = P384FieldElement::from_limbs(G_ODD_MULTIPLES_X[index]); + let y = P384FieldElement::from_limbs(G_ODD_MULTIPLES_Y[index]); + Some(if digit > 0 { (x, y) } else { (x, y.negate()) }) +} diff --git a/crypto/ec/src/p384_wnaf_table.rs b/crypto/ec/src/p384_wnaf_table.rs new file mode 100644 index 000000000..ffb18a5a4 --- /dev/null +++ b/crypto/ec/src/p384_wnaf_table.rs @@ -0,0 +1,297 @@ +//! The checked-in odd multiples of the base point `G` for [`crate::p384_wnaf`]'s fixed side: +//! `G_ODD_MULTIPLES_X[i]`/`G_ODD_MULTIPLES_Y[i]` are the affine coordinates of `(2i + 1) G`, for +//! `i` in `0..32`, as little-endian `u64` limbs -- the table a width-7 wNAF over `G` indexes. +//! `Q`'s table is built at run time because `Q` is the signer's key; `G` is the curve's constant, +//! so its table need not be. +//! +//! Generated in Python from the affine group law (SP 800-186 Appendix A.1.1) and SP 800-186 §3.2.1.4's `G`, +//! independently of this crate's arithmetic, and checked there by `n * G == infinity`; this +//! module's own test regenerates every entry with the crate's point arithmetic and compares. + +/// `x` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_X: [[u64; 6]; 32] = [ + [ + 0x3a545e3872760ab7, 0x5502f25dbf55296c, 0x59f741e082542a38, 0x6e1d3b628ba79b98, + 0x8eb1c71ef320ad74, 0xaa87ca22be8b0537, + ], + [ + 0x02d7e5c70500c831, 0xb408bbae5026580d, 0xbea4f240d3566da6, 0xcb9d3910202dcd06, + 0x64793c7e5fdc7d98, 0x077a41d4606ffa14, + ], + [ + 0x0abcdbc3836d84bc, 0x37882f4a1ca297e6, 0x4f6661cbe56583b0, 0xf208e51dbff98fc5, + 0x573cac5ea025e467, 0x11de24a2c251c777, + ], + [ + 0x040f05b48fb6d0e1, 0x8b05526f55b9ebb2, 0x2d58cc9dfa7b1c50, 0xad6fe997fbea5ffa, + 0xf29f8ebf234edffe, 0x283c1d7365ce4788, + ], + [ + 0x5c55e4461079118b, 0xc388528bfee2b953, 0xc6cb1ee285fb6e21, 0x2216f7291e6fd3ba, + 0xf1bf29b8b025b78f, 0x8f0a39a4049bcb3e, + ], + [ + 0x26356f3b55b4ddd8, 0x4749b66e3afb81d6, 0x56c9fd14892d3f8c, 0x7fe935ed5837c374, + 0xda1eeec2904816c5, 0x099056e27da7b998, + ], + [ + 0xaaf1ca1e3b5cbce7, 0x9ee5f441abd99f1b, 0x6267bcd1f0f11c13, 0x9632bff9f01f873f, + 0xafdaf5002ffcc6ab, 0xa567ba97b67aea5b, + ], + [ + 0x4b88701a9606860b, 0xa849557a10b6383b, 0x5b21f9f7da7c4e9c, 0x22a94156fff01c20, + 0x8cc15c11d8135255, 0xb3d13fc8b32b0105, + ], + [ + 0xcaf3a9add9ffcc03, 0x17012a991af1f486, 0xdcc614e42e5805f8, 0x692befb0733b41e6, + 0x00a5ebbcb13e1a32, 0x4099952208b48896, + ], + [ + 0x9599e68713f5d41b, 0x59c8651060801c0e, 0x12390b430467aabf, 0xe07dbecca86cd9b0, + 0x16858a211d750b77, 0x8d481dab912bc8ab, + ], + [ + 0x626b4c175eb77422, 0x46a37313df88fd64, 0xf9936136bf42cdb7, 0x606290985f8283f5, + 0xc7817121057d46e1, 0x27935df4e25c6f47, + ], + [ + 0x1e555ebc684aac81, 0x84a900a91f7c4ab5, 0xa68749c30c7f9aa4, 0x22c0102fd4293a78, + 0xe741a4a8000d5eb8, 0x3cb647a5df014c23, + ], + [ + 0x46f41be83716be9a, 0x31a8a58b3dcb2ca4, 0x555746bd28fda974, 0x07c4d76fab5ce0a3, + 0x9ee9eb5267946762, 0xe4c6d5cb9183a670, + ], + [ + 0x6b4eedfa23e2e546, 0x20761bc15898e1b0, 0x4acb49c0d7ba04e2, 0x08e3acaa2b4a8d3a, + 0x3c316d5a82c8b705, 0x9c5fa2c13f418e62, + ], + [ + 0x977bb83b1eb1a373, 0xad69e407f57b1e27, 0xfa46d04f4d4dfca2, 0x6e0af4f41cbcde50, + 0xd7b75810ce554ee5, 0xf1bc35f9656c6eea, + ], + [ + 0x0c8361dba9eb28b9, 0x061fa485bd1b8a65, 0x9d3993124d374e62, 0xc73e3336394c98c5, + 0x04fcfa12c087d446, 0xf2e6f06f0ea533e8, + ], + [ + 0xaefd7fec482c802a, 0x631eaed97add3fe2, 0xaf246b3f70bec90d, 0x55492715879f54bd, + 0xa92758f2773bb6c9, 0x38dbd58238c21bbc, + ], + [ + 0x29e4ae10ae1410ea, 0xc998b7fd7b3813ca, 0x55a0270e5859145d, 0xc3675bf4f728f3ac, + 0x9b9863d46535a5ab, 0x77c12dceca498ff9, + ], + [ + 0x2a07272d05de389d, 0xe56ba2d9cafcb448, 0x41c36c0122a10654, 0xd850ac1e45c8fcb9, + 0xfdb4300af2deaadc, 0x1221b1e1be96dfe8, + ], + [ + 0x15ac797ea8f8fedb, 0x4983e9bc8a47b24d, 0x35b72c15f264347c, 0x31c65d0bd2d3642c, + 0x8224c2dcc11702de, 0x9bc6b65436262167, + ], + [ + 0x10f3f39e87c76293, 0x077b05ca6a94f76d, 0x10d10234b609b8c7, 0x5185d456b279a184, + 0x411a5ed920a88b09, 0x2ce37b2917ad0d5c, + ], + [ + 0x80dddc249a26a33d, 0xaa49a4148fbebc67, 0xaba8ddfa4745a605, 0x4de52ef9689a5eb5, + 0xa5a2ff485cf5a64f, 0x9c3adb9111fe252c, + ], + [ + 0xa5b68a56bbfef50f, 0xb03d491de84ea016, 0x258449c2e28cd17c, 0xadd9c644fd8393e3, + 0x3bce43bec4f4609e, 0x6ce3bf11431d1d9c, + ], + [ + 0xd5254db0689a30fe, 0x21e9e8ccc22e6b2c, 0xe2a9530742a384f1, 0xc870bf64c76762c0, + 0xa06f4ca5fa30e0bc, 0x1273f595eaba5df1, + ], + [ + 0x9da43e8e5a0e8cf2, 0x8828f5b191d03d1c, 0xdd74d976a4911734, 0x7486cc53003ed182, + 0xf504f32fc1211afd, 0xa6c36459d1243ff7, + ], + [ + 0xffe06fbae0068fb0, 0xa618a8cc08d1b992, 0x7126ed3b510b0341, 0x0b4a2a1247e8f2cc, + 0xa86d2c9e23c2f5bc, 0x8bc6057da46fdafb, + ], + [ + 0x90903c9d776e9fa3, 0xff2ca2a90bdc08ac, 0x8f41cbe07c3e20b6, 0x88e935bdd5f169ba, + 0x92288353932cf408, 0xa0e5cd10144167c5, + ], + [ + 0x3bb25771ff2b4f85, 0x30d0a0e2e2ee7c87, 0xf22f519eeae1eb01, 0xf36d925a4f0112f5, + 0x307d064e7750ab08, 0xa059cd75571b3507, + ], + [ + 0x709d6a38b6ecb8bd, 0x07fd3823a2b10416, 0x78baa65996532279, 0x6c3eb8b272461fc7, + 0x4f1cec9879cabece, 0x8654ddc7eb10d5a8, + ], + [ + 0xba77d70f99fe20b0, 0xbbf7b15b29f59772, 0xa2b93e41e5a75d8a, 0x91e7691569202b98, + 0x4296f482176d1b01, 0xcc42cb6ad9f7161a, + ], + [ + 0x5c24cc10bda2cc75, 0xb5e5a6d1fb4071db, 0xa92542dbf5872edc, 0x5f729644b4fa011f, + 0x870b7900c1c47689, 0x0a12151e620a81a2, + ], + [ + 0xafd574f7e008f8af, 0x941560583def5431, 0x3c968cfbb83c8f7b, 0xac334769de358b33, + 0x2cae508f5164ca6c, 0xb32e89eb942aab11, + ], +]; + +/// `y` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_Y: [[u64; 6]; 32] = [ + [ + 0x7a431d7c90ea0e5f, 0x0a60b1ce1d7e819d, 0xe9da3113b5f0b8c0, 0xf8f41dbd289a147c, + 0x5d9e98bf9292dc29, 0x3617de4a96262c6f, + ], + [ + 0xb65f28600a2f1df1, 0xc24abd6be4b5d298, 0xf7684c0edc111eac, 0x8520b41c85115aa5, + 0x7d0bbe9602a9fc99, 0xc995f7ca0b0c4283, + ], + [ + 0x184414abe6c1713a, 0x3177686d0ae8fb33, 0x8c986533b6901aeb, 0x284b447754d5dee8, + 0x0f5837e90a00e7c5, 0x8fa696c77440f92d, + ], + [ + 0x64664cdac512ef8c, 0x30d84ede32a78f9e, 0xd9c92cd01dbd2256, 0x1a61d867ed799729, + 0xba52efdb8c169047, 0x9475c99061e41b88, + ], + [ + 0x262da4f9ac664af8, 0x9e743efedfd51b68, 0xb7678854aed9b302, 0x9a9b3d7ca3c400c6, + 0x452c4a5322c3a979, 0x62c77e1438b601d6, + ], + [ + 0x7d5dba8138c5e0bb, 0x5466d51263aaff35, 0x43ff93f41b52a325, 0x6fc4eed8dfc363fd, + 0x688505544ac5e039, 0x2e4c0c234e30ab96, + ], + [ + 0x6423a12736f429cc, 0x776bcb8272218a7d, 0x86329be057857d66, 0x5185595046932ec0, + 0x644e4147af164ecc, 0xde1b38b3989f3318, + ], + [ + 0x985d588d33f7bd62, 0x838d24f8b284af50, 0x84d1114373dfbfd9, 0xeebac4a11d749af4, + 0x1b049b2536164b1b, 0x152919e7df9162a6, + ], + [ + 0xfa650ef5e23b09a0, 0x04da4d2faa9680ec, 0x9b797d277f2388b3, 0x163ad3f8008ad0cd, + 0x39474594af603598, 0x5ecf947778330598, + ], + [ + 0x6995b07e75e52245, 0x11ffba5608004e64, 0x594b32fd7adc0e8c, 0xb2291b68a1039aa0, + 0x7be99f2a60669050, 0xa1592ff012146085, + ], + [ + 0x9d5606eb10c69f84, 0x2b6f8d9838ef0c13, 0x585d9c4e6c615b53, 0xa4f2d4aec079c6b7, + 0x9ab1a3798d1e3524, 0x380a1a3b4898d4cd, + ], + [ + 0x79b65fea0d5a2b14, 0x09be21f6414b9be2, 0x7afbf0557ead6368, 0x6103c7b0218bc837, + 0x4966f85ebdc18cb4, 0x28300479a8f88dd0, + ], + [ + 0xffaf54d7e6aac897, 0x09a957bbac49722c, 0x6280b8afe5d6a5a2, 0x5942e18d15922f20, + 0xc0027f165dd74ca9, 0xa37245523aa57845, + ], + [ + 0x529fb56479f48752, 0x0ef2c4b2213f519b, 0x6f015651bd9218e3, 0x94fdf1b7da9e5955, + 0x5afe81503cb7bf61, 0xf641de0cb075851a, + ], + [ + 0x53e0d05ef28faf29, 0x3ffe8f5147848a0e, 0x531f70960a30e838, 0x8d7aec776d94fc70, + 0xf733c547c0f2b3dc, 0x2458497a559bcdcc, + ], + [ + 0x3152f5ba7852680c, 0x3e8802c6d2fc9ebf, 0x3fb0b9ac114251cd, 0x4a041a8651780d31, + 0xf9e442612fcffbd1, 0x8f86e464c200bc46, + ], + [ + 0x599a066726d333bb, 0xd3145ea7abbf4370, 0x7b86f2aba9bd9527, 0xeff54a716511f5f1, + 0x12a77e148373d30f, 0x828c26694df81889, + ], + [ + 0x35747e089cbeef77, 0xc20c5f0bbef22673, 0x00487a48cee2b22c, 0x396e7fb8964f4304, + 0xd302f0d0bd10269e, 0xeee351bc34276d3a, + ], + [ + 0x533ffb21939dd273, 0x5f4f2a9231bf6723, 0xb233124188d17ebb, 0x059953dd8965d97d, + 0x1ad8460e2e216618, 0x50b225fa8a5ed838, + ], + [ + 0xc0979304abbcfcd4, 0x6ad799aed585d888, 0x22e5893043a248bf, 0xdba18218185770c7, + 0x7147eed091efb368, 0x4482f093849864ec, + ], + [ + 0xcf8a7d7be2a7607e, 0x4dda48b4fa5d1dfd, 0x8208f6db859227bb, 0xbab5b7b88f803547, + 0xdabde6f484adc04f, 0xbf27e3e07ba0ebb9, + ], + [ + 0x344000dc47355061, 0xdbae5b8175af7420, 0x4c69c4c0108a6a32, 0x69dcb8452f057663, + 0x50b9f80b22b4829a, 0x83e8d95f813af4db, + ], + [ + 0x36dde9917c570471, 0x8105da32421e5cfe, 0x58d74fe2bc8777e0, 0x918800e37b4e4aa8, + 0x7c76075539542abd, 0xc3325e274b23604e, + ], + [ + 0xcbdaf9963d58f4ce, 0xa7147a032edfc8b0, 0x13cce2421581486c, 0xa1afe32ae0ca83c2, + 0xa0db552ca682b49f, 0x62737fc8fe0818cb, + ], + [ + 0x2e6ddb768c63b40a, 0x38a1bc0bc57b89f6, 0x22899f57821ad25b, 0x0bd72553fa3365b4, + 0x1cebd105c28a71d0, 0x58279fcdf96c71d9, + ], + [ + 0x6cb597b8e22c2ce9, 0x54c6d084dd6077f5, 0x42fed20299ce9705, 0x777fe7595994ae0d, + 0xfbf2a22fd1f1985d, 0xfea19f27f6881a60, + ], + [ + 0xfc5c555ade9e587e, 0x0c9dcbea9d4197aa, 0xe7dbf0ae17f7fd0a, 0xd6396c943a16024e, + 0xaa542f5c1e7ce4be, 0x120de79718d154fe, + ], + [ + 0xaec87aa569c8d6cc, 0xa880c76b8849bcd5, 0x0fa0147d878235aa, 0xfc311351556408ff, + 0x026d8cebfcdfc32e, 0x40f6ae62c5a75f70, + ], + [ + 0xb867bea8e3578549, 0x88d6dbd5abb99b51, 0x7d1d13d1aa1c7e40, 0x742599e648fd7ea4, + 0x830567af763ec9d0, 0x3127066c11092e88, + ], + [ + 0xda74d5345e24d63f, 0x182916746338781d, 0xc23168cdfb54deb9, 0xea4223f449e3895e, + 0x91646e65069fa685, 0xbbd6bb6bad8516d3, + ], + [ + 0xa13e157ec5a3e83c, 0x84fb986767fb8c48, 0xcf6593451a9a0698, 0x4479d4c75bb1d9f1, + 0xe15002475bc0a80c, 0x857c802960eaebb7, + ], + [ + 0xc81abe0944e11e2c, 0x35daedff59e74a1a, 0x79cddd7a04990095, 0xa770fc48b73edc06, + 0x24e2bf69603ffd5b, 0xb1e3ae7166d97103, + ], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::p384::P384FieldElement; + use crate::p384_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::p384_point::P384JacobianPoint; + + #[test] + fn table_matches_the_odd_multiples_of_g_computed_by_the_point_arithmetic() { + let g = P384JacobianPoint::from_affine( + P384FieldElement::from_limbs(G_X_LIMBS), + P384FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + let mut cur = g; + for i in 0..32 { + let (x, y) = cur.to_affine().expect("an odd multiple of G below n is never infinity"); + assert_eq!(x.to_limbs(), G_ODD_MULTIPLES_X[i], "entry {i}: x of {}G", 2 * i + 1); + assert_eq!(y.to_limbs(), G_ODD_MULTIPLES_Y[i], "entry {i}: y of {}G", 2 * i + 1); + cur = cur.add_vartime(&two_g); + } + } +} diff --git a/crypto/ec/src/p521.rs b/crypto/ec/src/p521.rs new file mode 100644 index 000000000..91bd95bf1 --- /dev/null +++ b/crypto/ec/src/p521.rs @@ -0,0 +1,331 @@ +//! The P-521 base field, GF(p) for `p = 2^521 - 1` (a Mersenne prime). +//! +//! Domain parameters are from NIST SP 800-186 (Feb 2023) §3.2.1.4, "Curve P-521". [`P_LIMBS`] +//! holds `p` in little-endian `u64` limbs, 9 limbs (576 bits of storage) for the 521-bit value; +//! the top limb only ever uses its low 9 bits (`0x1ff`). +//! +//! # Reduction algorithm +//! +//! Same fold shape as [`crate::p256`] and [`crate::p384`]: `2^521 = p + 1`, i.e. `C = 1`, the +//! simplest possible case (no widening multiply needed for the fold at all -- just an add). For a +//! product `t = a*b` with `a, b < p < 2^521` (so `t < 2^1042`): fold 1 splits `t` into `hi0 = +//! t >> 521 < 2^521` and `lo0 = t mod 2^521 < 2^521`, giving `acc1 = hi0 + lo0 < 2^522`; fold 2 +//! splits `acc1` into `hi1 = acc1 >> 521 < 2` and `lo1 = acc1 mod 2^521 < 2^521`, giving `acc2 = +//! hi1 + lo1`. Unlike [`crate::p384::reduce`] (whose analogous two-fold bound leaves room for a +//! rare extra bit above `2^521`'s P-384 equivalent -- see that function's comment for the bug this +//! caused there), `acc2` here is *provably* always `< 2^521`: `acc1 <= 2*(2^521 - 1) = 2^522 - 2` +//! (both `hi0` and `lo0` are `<= 2^521 - 1`), which excludes `acc1 = 2^522 - 1`, the only value +//! that would let `hi1 = 1` and `lo1 = 2^521 - 1` hold simultaneously and push `acc2` to exactly +//! `2^521`. So two folds always leave a clean `< 2^521` result, needing only the usual single +//! conditional subtraction of `p` (verified, not checked in, against 500,000 random trials plus +//! the `(p-1)*(p-1)` worst case, in addition to the proof above). + +use crate::nat; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// `p`, little-endian `u64` limbs (SP 800-186 §3.2.1.4); the top limb only uses its low 9 bits. +pub const P_LIMBS: [u64; 9] = [ + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, +]; + +/// `p - 2`, little-endian `u64` limbs -- the public exponent [`P521FieldElement::invert`] raises +/// its base to, per Fermat's little theorem. +const P_MINUS_2_LIMBS: [u64; 9] = [ + 0xfffffffffffffffd, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, +]; + +/// An element of the P-521 base field GF(p), always held in canonical reduced form (`< p`). +#[derive(Clone, Copy, Debug)] +pub struct P521FieldElement([u64; 9]); + +impl P521FieldElement { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0, 0, 0, 0, 0, 0]); + + /// The multiplicative identity. + pub const ONE: Self = Self([1, 0, 0, 0, 0, 0, 0, 0, 0]); + + /// Builds a field element from little-endian `u64` limbs, accepting the type's *entire* + /// 576-bit storage range (unlike [`crate::p256::P256FieldElement::from_limbs`] and + /// [`crate::p384::P384FieldElement::from_limbs`], whose native limb width matches their + /// curve's bit width exactly, this one has 55 spare bits above `p`'s 521). Simply masking + /// those spare bits away would silently discard them -- `p + 1 = 2^521` sets only bit 521, so + /// masking it off would wrongly produce `0` instead of correctly reducing to `1` -- so instead + /// the top limb's bits 9-63 (`>> 9`, at most 55 bits) are folded back in via `2^521 ≡ 1 (mod + /// p)`, the same identity [`reduce`] uses: the fold's sum is `< 2^521 + 2^55`, comfortably `< + /// 2p`, so one conditional subtraction still suffices. + pub fn from_limbs(limbs: [u64; 9]) -> Self { + let hi_val = limbs[8] >> 9; + let mut lo = limbs; + lo[8] &= 0x1ff; + let (folded, carry) = nat::add(&lo, &[hi_val, 0, 0, 0, 0, 0, 0, 0, 0]); + debug_assert_eq!(carry, 0, "P-521 from_limbs fold overflowed 9 limbs"); + let (diff, borrow) = nat::sub(&folded, &P_LIMBS); + let mut result = [0u64; 9]; + ct::conditional_select(Condition::::from_lsb(borrow), &folded, &diff, &mut result); + Self(result) + } + + /// Returns the canonical little-endian `u64` limbs, `< p`. + pub fn to_limbs(&self) -> [u64; 9] { + self.0 + } + + /// The limbs exactly as this type stores them: the canonical value itself, so this is + /// [`Self::to_limbs`] by another name. Paired with [`Self::from_internal_limbs`] so the + /// crate's point arithmetic can select between the coordinates of two existing points without + /// re-reducing each one. + pub(crate) fn internal_limbs(&self) -> [u64; 9] { + self.0 + } + + /// Wraps limbs already in this type's stored representation (see [`Self::internal_limbs`]), + /// skipping the reduction and conversion [`Self::from_limbs`] performs. Only for values that + /// came out of another element of this type, or a constant computed in that representation: + /// the masked selects in the point arithmetic, and the comb table's infinity sentinel. Not + /// exposed, since an out-of-range value would break every equality and reduction assumption + /// downstream; the range is checked in debug builds. + pub(crate) fn from_internal_limbs(limbs: [u64; 9]) -> Self { + debug_assert_eq!( + crate::nat::sub(&limbs, &P_LIMBS).1, + 1, + "from_internal_limbs given a value >= p" + ); + Self(limbs) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + nat::is_zero(&self.0) + } + + /// Overwrites the element with zero, for scrubbing a secret-derived value before it goes out + /// of scope. Defence in depth rather than a guarantee: copies the compiler kept in registers + /// or spilled itself are not reached. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self + other mod p`. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = nat::add(&self.0, &other.0); + // true_sum = sum + carry*2^576; self,other < p < 2^521, so true_sum < 2p < 2^522, and the + // carry out of the 9-limb (576-bit) add can only ever be 0 in practice (2p < 2^576 with + // huge headroom) -- kept for structural symmetry with the other curves' `add`, and + // debug-asserted rather than silently trusted. + debug_assert_eq!(carry, 0, "P-521 add overflowed 576 bits, which should be unreachable"); + let (diff, borrow) = nat::sub(&sum, &P_LIMBS); + let mut result = [0u64; 9]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut result); + Self(result) + } + + /// `self - other mod p`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = nat::sub(&self.0, &other.0); + let (corrected, _) = nat::add(&diff, &P_LIMBS); + let mut result = [0u64; 9]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod p`. + pub fn negate(&self) -> Self { + let (diff, _) = nat::sub(&P_LIMBS, &self.0); + let mut result = [0u64; 9]; + ct::conditional_select(nat::is_zero(&self.0), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod p`. + pub fn mul(&self, other: &Self) -> Self { + Self(reduce(&widening_mul(&self.0, &other.0))) + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`widening_square`]. + /// + /// Used by the exponentiation loops ([`Self::invert`] and friends), which call it hundreds of + /// times in a row with nothing else competing for registers. Deliberately *not* used by this + /// curve's point arithmetic, which squares by calling `mul` with equal arguments: that was + /// tried and measured on P-256, and despite `square` being ~9% cheaper than `mul` in + /// isolation it made the constant-time comb multiplier consistently slower, because inlining + /// a third wide-multiply routine into that loop costs more than the saved partial products + /// return. See [`crate::p256::P256FieldElement::square`] for the numbers. + pub fn square(&self) -> Self { + Self(reduce(&widening_square(&self.0))) + } + + /// `self^-1 mod p`, or `0` if `self` is `0`. Fermat's little theorem over the public + /// exponent `p-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..9).rev() { + let limb = P_MINUS_2_LIMBS[limb_idx]; + // The top limb holds only 9 bits of the exponent, in nibbles 0..=2; its higher + // nibbles are zero and are skipped rather than spent on squarings of `1`. + let nibbles = if limb_idx == 8 { 3 } else { 16 }; + for nibble_idx in (0..nibbles).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } +} + +impl PartialEq for P521FieldElement { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..9 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for P521FieldElement {} + +/// Schoolbook widening multiply of two 9-limb operands (of which only the low 521 bits are ever +/// nonzero) into an 18-limb product. +fn widening_mul(a: &[u64; 9], b: &[u64; 9]) -> [u64; 18] { + let mut result = [0u64; 18]; + for i in 0..9 { + let mut carry: u128 = 0; + for j in 0..9 { + let idx = i + j; + let prod = (a[i] as u128) * (b[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + // Mutating `carry = s >> 64` here (e.g. to `<< 64`) is an accepted mutant, not a bug: this + // loop's carry is always `0` or `1` at each step (`s` sums two u64-range values, so `s < + // 2^65`), and a value produced by `<< 64` has all its bits at position 64 or above -- so it + // contributes nothing to `result[k'] as u128 + carry`'s low 64 bits at the next iteration, + // `result[k']` is written unchanged, and the "carry" that comes back out is again pure + // high bits, propagating forever without ever affecting `result` (which is all this + // function returns; `carry` itself is discarded once the loop ends). This holds for any + // input, not just this crate's own `< p` values -- confirmed both by this argument and by + // hand-mutating this exact line and finding no test (including a version instrumented to + // panic whenever this loop's incoming carry is nonzero, which it regularly is) notices. + let mut k = i + 9; + while k < 18 { + let s = (result[k] as u128) + carry; + result[k] = s as u64; + carry = s >> 64; + k += 1; + } + } + result +} + +/// Schoolbook squaring of a 9-limb value into its 18-limb square. +/// +/// `a * a` is symmetric: the product `a_i * a_j` appears twice for every `i != j`. Forming each of +/// those once and doubling costs `L(L+1)/2` limb multiplications -- 45 at this width against +/// the 81 [`widening_mul`] would form for the same value. +/// +/// Three passes: the off-diagonal products `a_i * a_j` for `i < j`; a doubling of the whole +/// accumulator; then the diagonal squares `a_i * a_i` added in at limb `2i`. Writing `result[i + +/// 9]` in the first pass is an assignment rather than an accumulation because row `i` only ever +/// reaches limbs `2i + 1 ..= i + 9 - 1`, and no earlier row reaches limb `i + 9` either, so that +/// limb is still zero when the row's final carry lands on it. The doubling cannot overflow: the +/// off-diagonal sum is strictly less than `a^2 / 2`. +/// +/// Verified against Python's arbitrary-precision `**2` over 20,000 random 9-limb values plus the +/// all-ones worst case before being written here. +fn widening_square(a: &[u64; 9]) -> [u64; 18] { + let mut result = [0u64; 18]; + + // Off-diagonal products, each formed once. + for i in 0..9 { + let mut carry: u128 = 0; + for j in (i + 1)..9 { + let idx = i + j; + let prod = (a[i] as u128) * (a[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + result[i + 9] = carry as u64; + } + + // Every off-diagonal product appears twice in the square. + let mut carry = 0u64; + for limb in result.iter_mut() { + let next_carry = *limb >> 63; + // `*limb << 1` always leaves bit 0 clear and `carry` is only ever 0 or 1, so the two + // operands are disjoint: mutating this `|` to `^` is an accepted equivalent, not a bug. + *limb = (*limb << 1) | carry; + carry = next_carry; + } + debug_assert_eq!(carry, 0, "doubling the off-diagonal sum overflowed 18 limbs"); + + // Diagonal squares. + let mut carry: u128 = 0; + for i in 0..9 { + let square = (a[i] as u128) * (a[i] as u128); + let low = (result[2 * i] as u128) + ((square as u64) as u128) + carry; + result[2 * i] = low as u64; + let high = (result[2 * i + 1] as u128) + (square >> 64) + (low >> 64); + result[2 * i + 1] = high as u64; + carry = high >> 64; + } + debug_assert_eq!(carry, 0, "adding the diagonal overflowed 18 limbs"); + + result +} + +/// Reduces an 18-limb (1042-bit-capacity) value modulo `p`, per the fold described in the module +/// docs: `2^521 ≡ 1 (mod p)`, so each fold is a plain add of the high and low 521-bit halves. +fn reduce(t: &[u64; 18]) -> [u64; 9] { + let mut acc: [u64; 18] = *t; + for _ in 0..2 { + // Split at bit 521: the low 8 limbs plus the low 9 bits of limb 8 are `lo`; the rest of + // limb 8 (bits 9-63) plus limbs 9..18 form `hi`, shifted down by 521 bits. + let mut hi = [0u64; 9]; + for i in 0..9 { + // `acc[8+i] >> 9` fills bits 0-54 of the result; `acc[9+i] << 55` fills bits 55-63 + // (only its own low 9 bits survive the shift) -- disjoint, so `|` and `^` agree here. + hi[i] = (acc[8 + i] >> 9) | (acc[9 + i] << 55); + } + let mut lo = [0u64; 9]; + lo[..8].copy_from_slice(&acc[..8]); + lo[8] = acc[8] & 0x1ff; + + let (sum, carry) = nat::add(&hi, &lo); + debug_assert_eq!(carry, 0, "P-521 reduction fold overflowed 9 limbs"); + acc = [ + sum[0], sum[1], sum[2], sum[3], sum[4], sum[5], sum[6], sum[7], sum[8], 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ]; + } + let low: [u64; 9] = [acc[0], acc[1], acc[2], acc[3], acc[4], acc[5], acc[6], acc[7], acc[8]]; + let (diff, borrow) = nat::sub(&low, &P_LIMBS); + let mut result = [0u64; 9]; + ct::conditional_select(Condition::::from_lsb(borrow), &low, &diff, &mut result); + result +} diff --git a/crypto/ec/src/p521_comb.rs b/crypto/ec/src/p521_comb.rs new file mode 100644 index 000000000..82a052c38 --- /dev/null +++ b/crypto/ec/src/p521_comb.rs @@ -0,0 +1,73 @@ +//! Fixed-base comb scalar multiplication: `[k]G` for P-521's base point `G`, with `k` secret. +//! Identical algorithm to [`crate::p256_comb`] -- see that module's docs for the full derivation +//! and verification methodology -- except `D * WIDTH = 87 * 6 = 522` here, one bit more than +//! `BITS = 521` (a single bit of zero-padding, same idea as P-256's two). +//! +//! Verified (not checked in) against 200 random `k` plus edge cases (`k = 1, 2, 3, n-1`), each +//! cross-checked against the SP 800-186 Appendix A.1.1 affine group law. + +use crate::p521::P521FieldElement; +use crate::p521_comb_table::{COMB_TABLE_X, COMB_TABLE_Y}; +use crate::p521_point::P521JacobianPoint; +use crate::p521_scalar::P521Scalar; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 6; +const BITS: usize = 521; +const D: usize = BITS.div_ceil(WIDTH); +const FULL_COMB: usize = D * WIDTH; +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]G`, branch-free in `k`. +pub fn comb_multiply_base_point(k: &P521Scalar) -> P521JacobianPoint { + let limbs = k.limbs(); + let mut r = P521JacobianPoint::INFINITY; + let top = (FULL_COMB - 1) as isize; + for i in 0..D { + let mut secret_index: usize = 0; + let mut j = top - i as isize; + while j >= 0 { + // `< BITS` vs `<= BITS` only differ at `j == BITS` (521): both give `0` there in + // practice, since `limbs` is always a canonical (`< n < 2^521`) scalar, whose bit 521 + // is `0` regardless of which branch reads it -- an accepted mutant, not a bug. + let bit = if (j as usize) < BITS { + (limbs[(j as usize) / 64] >> ((j as usize) % 64)) & 1 + } else { + 0 + }; + // `secret_index << 1`'s vacated low bit and `bit` (always `0` or `1`) are disjoint, so + // `|` and `^` agree here. + secret_index = (secret_index << 1) | (bit as usize); + j -= D as isize; + } + r = r.double(); + let (x, y, is_infinity) = table_lookup(secret_index); + r = r.add_affine(&x, &y, is_infinity); + } + r +} + +/// Looks up comb table entry `secret_index`, scanning every one of the [`TABLE_SIZE`] entries +/// under a mask. See [`crate::p256_comb::table_lookup`]'s docs. +fn table_lookup(secret_index: usize) -> (P521FieldElement, P521FieldElement, Condition) { + let mut x_limbs = [0u64; 9]; + let mut y_limbs = [0u64; 9]; + for i in 0..TABLE_SIZE { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + let mut next_x = [0u64; 9]; + ct::conditional_select(is_this_entry, &COMB_TABLE_X[i], &x_limbs, &mut next_x); + x_limbs = next_x; + let mut next_y = [0u64; 9]; + ct::conditional_select(is_this_entry, &COMB_TABLE_Y[i], &y_limbs, &mut next_y); + y_limbs = next_y; + } + // Entry 0 is the point at infinity; every other entry is affine (Z == 1). The accumulator's + // `add_affine` takes the identity as a flag rather than as a `Z == 0` coordinate. + let is_infinity = Condition::::is_equal(secret_index as u64, 0); + ( + P521FieldElement::from_internal_limbs(x_limbs), + P521FieldElement::from_internal_limbs(y_limbs), + is_infinity, + ) +} diff --git a/crypto/ec/src/p521_comb_table.rs b/crypto/ec/src/p521_comb_table.rs new file mode 100644 index 000000000..247eae9d6 --- /dev/null +++ b/crypto/ec/src/p521_comb_table.rs @@ -0,0 +1,715 @@ +//! The checked-in fixed-base comb table for `[k]G` (P-521's base point `G`), width 6, same choice +//! as [`crate::p256_comb_table`] (the standard width for fields over 250 bits). See +//! [`crate::p521_comb`] for the multiplier that uses this table. +//! +//! `COMB_TABLE_X[i]`/`COMB_TABLE_Y[i]` are the affine `(x, y)` coordinates of the `i`-th table +//! entry, as little-endian `u64` limbs; entry `0` is the point at infinity (`x = y = 0`). +//! +//! Regenerated and compared against these checked-in values by this module's own `tests` +//! submodule below -- see [`crate::p256_comb_table`]'s docs for why this is a unit test, not an +//! integration test. + +pub(crate) const COMB_TABLE_X: [[u64; 9]; 64] = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [ + 0xf97e7e31c2e5bd66, 0x3348b3c1856a429b, 0xfe1dc127a2ffa8de, 0xa14b5e77efe75928, + 0xf828af606b4d3dba, 0x9c648139053fb521, 0x9e3ecb662395b442, 0x858e06b70404e9cd, + 0x00000000000000c6, + ], + [ + 0xb4c52f11f3d7286a, 0xec47cb5cbbf67a41, 0x89d3382eaafb10a4, 0x4afd876806222b90, + 0xfa16b295feb5416d, 0x0946f9d45d095b7c, 0x18526b28ff975e3d, 0x259cec2fb2f1c609, + 0x0000000000000104, + ], + [ + 0xdae82f27eb2db12f, 0x675f5cdb25444b98, 0x3234752e78288513, 0x53e37a78730f5769, + 0xabad04dadcecd88d, 0x29d87958f3098a34, 0x00567f1a50cb0363, 0x400b67853878a6f6, + 0x0000000000000183, + ], + [ + 0x0cfa67a640b0a066, 0xe97c179e8057a710, 0xb69d285227cce600, 0xf7dde8c5419687d1, + 0xdbcb15185d5b039e, 0x3e47772fd99abbe0, 0x71c9c23ab2a9c6a0, 0x5af10b9b94496951, + 0x0000000000000152, + ], + [ + 0x36005a37c7b848a0, 0xbac203eee032cc07, 0x8d20246ee4c229c4, 0x13b8b0f97b7f6b06, + 0x0223a119e13c7b1d, 0xa91307c053c2e32a, 0xce595e9f3536fe07, 0xc2a2efbc8489551d, + 0x0000000000000019, + ], + [ + 0xfd0c67177af706c0, 0xc500416552ebdf4a, 0x332675708ff0bf03, 0x3f282dddd2cdcf58, + 0x93b60398bf766c2f, 0x124f1fd970748a8c, 0x05422ecb20ef5bad, 0x9fef00a63eafb2dc, + 0x00000000000001d8, + ], + [ + 0xa7b7271a71b90b8c, 0xbdc1bcdad12ca22a, 0x52a5f2901f10a310, 0xf5c070c0f420fb28, + 0xdd09d3a5f099a78f, 0xaf60cbbe3914e826, 0xeac320c9a914d69f, 0x78bc962dfd5ba85d, + 0x00000000000001c0, + ], + [ + 0xc2453d609102b570, 0x70840e5c142c3e23, 0x7294b1f1c30037d9, 0xa2500f121dbf5ab6, + 0x03eceb67c9c8ee6e, 0xa19c319330820abb, 0x7aad59e87da6b820, 0xe97823f8c38d842f, + 0x000000000000006d, + ], + [ + 0x5ba6aa8d478bab98, 0x849ddc2dbcf097c4, 0x67f6f4b274b08601, 0xb15c438fe3acbdac, + 0x0bcd49256ee2c337, 0xd15facb9b3085e64, 0xc8e919dcf4d1b708, 0x6eed3e36e1fae449, + 0x000000000000000a, + ], + [ + 0xe2fa0b39d0851f6a, 0x2f7f00e5263539c6, 0x46a066b81f107130, 0xe7cc14f87dbe9579, + 0x61f1e5c2d0607b55, 0x539a9c4d07a4c0b8, 0x9b6893829fdc8d6f, 0x684725a9847245fc, + 0x0000000000000020, + ], + [ + 0xa732fe002dcd1035, 0x034d6ff8da1f6ee4, 0x2103d3e5097d7909, 0xcc496507dfbec358, + 0x27044faeef335708, 0xf685e6bffe9be99a, 0x57b8c6c406aababd, 0xb8e751f6a901830c, + 0x000000000000011b, + ], + [ + 0x689a234ba08e3299, 0x941f6f05c4cf87aa, 0x3a98121b9ffdcb65, 0x3252b8481623b72c, + 0x65f531b5c6bae311, 0x4c5005a1c383df2c, 0x13c011795826c457, 0x90ea82a2ee3b03de, + 0x00000000000001d3, + ], + [ + 0x4ab1603c78f459e6, 0x7d9af2dc6a84373e, 0x2d2be036159a9a40, 0x82371e3d509c38ec, + 0x82188bf292f079b7, 0xd74b82801e82e017, 0x4041778a6b59bbfa, 0x40cf824ab8d4f9a7, + 0x000000000000005d, + ], + [ + 0x0eedf7150a80f0b9, 0x7f78ee5beb322905, 0x15d5375783498544, 0xff081204b66d249a, + 0xb95434862bd0d1fc, 0x6404f1321899b952, 0x1de3aa0ccdd8789c, 0x8b6f01381a7153a4, + 0x00000000000001be, + ], + [ + 0x5141181078303dc6, 0x93d1836de01c1d96, 0x98046c2ff50b037b, 0xeb3a5b31fcd53e6c, + 0x3976f8fefb2f8250, 0xd193f5369cda2685, 0xc609817dd26ee74c, 0xeea2244858f903d3, + 0x00000000000000e9, + ], + [ + 0x1e2df23635210df1, 0x387b5c569aace566, 0xdd8152c9ee401323, 0x56fe9ec99cac0076, + 0x88ed7fc1e1a9f782, 0x6551487d681f0428, 0xeaacc10197689006, 0x626bcad13b7e7fe6, + 0x000000000000015e, + ], + [ + 0x66bd9a0dcd698697, 0x0e7d8559bbbc1758, 0xe072a980ac76a88d, 0x3cf9af4de2c17874, + 0x85b24bc4748e97ad, 0xe5522faff3f6d8ea, 0x60ed43eecef45795, 0x1859fd20de38467d, + 0x00000000000001d7, + ], + [ + 0x6fa722771bac59f3, 0x72358865520791ad, 0x8da1c8c4e32463d8, 0xe0e75863d41001a5, + 0xcea164f748e602db, 0x93cb5f21cc294cfe, 0x28a3168a941cbefa, 0x018fff3bab807177, + 0x00000000000001e0, + ], + [ + 0x728e1148bdbc363a, 0xdfbf9e0bf7a1bbab, 0xd6fcfc8fa2e12868, 0x3969e7e50a19fe81, + 0xf104d3bef512cd5e, 0x9879c459810da8f5, 0xad31c365924bf3a3, 0xb2e2d4914d4fb575, + 0x0000000000000151, + ], + [ + 0x954376cee18bd1d3, 0x350613fc9c8e4d95, 0xa60d595673323968, 0x4f6ac836a044cdbc, + 0xf38d747b881b8e40, 0x79f7faf00426afe2, 0xf6e7403d2ae12787, 0x721445a6726ae667, + 0x0000000000000141, + ], + [ + 0x55a5e8f4641b7e08, 0xe1e589f3caa920f7, 0x07b90ad9abfced83, 0x05ff78cd7c466f08, + 0x2020ab945a71ab69, 0xd3ad1f5787ee2ec2, 0x078ba17efb3d4c91, 0xcf3753ba8607cf69, + 0x0000000000000138, + ], + [ + 0x071492bba1e06f89, 0x6f3f72de734cc62a, 0xb50a11309d477637, 0x5f6b0546a5ce54f0, + 0xc1cde769698626ed, 0xe43af79564ade3d0, 0x3067161797a20d2b, 0xdf5821678c48495b, + 0x0000000000000129, + ], + [ + 0x2fdf67bf4f6f867e, 0x56f6ea005f1e2bf2, 0xa7d7c0d8896ab990, 0x589102a023329acb, + 0xbb8b6a64156a7f42, 0xfbb556a95882578a, 0xa82481cc609250dd, 0x573e51d9daad3d36, + 0x000000000000005d, + ], + [ + 0xd3a8c547c7d40b83, 0xd9fd110285002a83, 0x4bd9516c97f61840, 0x8c628537448c4851, + 0x5cae5aa1c90925ad, 0x356195c572319740, 0x8cb6e19ba2b5be38, 0x68626a4b6d9c8c33, + 0x00000000000001c5, + ], + [ + 0xdb0fa258898a19c2, 0x26346630974ccc01, 0x135ec9db1f1e6a83, 0x9bac9c9bff494d1b, + 0x78d21ad4f24be4d7, 0x3249c89f61e8dacd, 0x528df6852df2cb51, 0x4f0ec6792ccdc5b7, + 0x00000000000001db, + ], + [ + 0xdf886f2283fe19b9, 0x740586885dba4f8e, 0x3c04af37a13f8dc7, 0x21771e2b3b99dcbc, + 0x1e77950d55c5b6a7, 0x20a6975b1b1338e1, 0x4956e06bd276d114, 0x56686808b0d3b14c, + 0x000000000000016c, + ], + [ + 0x953bb441f640de23, 0x5d35f0a9e1e08dc6, 0x323a1bcc7bbad4e6, 0x4dc31447742e5ad0, + 0xca0e8e15068bf07d, 0xfc317d5097ebd2b5, 0xf5447997db4c9342, 0xf98a3ac80ba0a29c, + 0x0000000000000061, + ], + [ + 0xa2d5fdb981e3d610, 0x0127b05c456ddf1c, 0x29e247d9bd5bd8a4, 0x5758d126f5959d68, + 0x9b0fb7fb2b28eb19, 0x1d8aa23a1d312140, 0xf8b9e467281b4d2c, 0xea57464cda959b8f, + 0x0000000000000028, + ], + [ + 0xa15c4a20a5eff951, 0x1baa735b49f4f32f, 0xab53ad000feaf2c6, 0x354c2bc4ed66b803, + 0x587fd2379ac12f3a, 0x3837c9643881a729, 0x1d8bd95b9f6c3bbe, 0x237ed2d808d914b6, + 0x000000000000001c, + ], + [ + 0x3f621de51cf1a11f, 0x18b004fd0658152d, 0x1bc8a9455df479b5, 0x901719473da586ac, + 0x06fe325baa2f052f, 0x4a2fc982b6323a69, 0xa975e8bcd4f0b34c, 0xfe3f271914ed1d56, + 0x000000000000001f, + ], + [ + 0xb7f2a894410a34a1, 0xca6d4597f0e4d4f6, 0x84c4a0fe6d4d1f8f, 0x42d465bb405c281d, + 0x44893a203587a898, 0xc0d0d12dab4ffdd6, 0xd5f85269fc00e886, 0xd93f9cdce1999ae1, + 0x0000000000000199, + ], + [ + 0x3a44dde085f45edd, 0xc543bb6498309156, 0x82d1ce827597d0d0, 0xb2dc0f228106ca3e, + 0x66f394472c3a313a, 0xd8f6fcdaa6cf2e03, 0xed6e0e279e6bf40b, 0x7dcd3a305b9a41af, + 0x0000000000000095, + ], + [ + 0xaff40f1bb5598dcc, 0xef04d8e02cd12fd9, 0xc97ea3b0bad7f4ac, 0x336776a6565108cd, + 0x71d9d056fe63178c, 0x31a7c54714c3f6aa, 0x5da757a2f6804c72, 0x7b3ee12ff87bb311, + 0x00000000000001fa, + ], + [ + 0xffef7d4f0399f8cc, 0x1e2708d99da4c3fa, 0x5792ebd0a9474303, 0x21aa73f56812a436, + 0xf61f60fbc052973d, 0x7061be89fc874833, 0xe65c7ebe2638329d, 0xe728d78b032852d9, + 0x00000000000001e5, + ], + [ + 0x94de21d800df95b9, 0x5d5301b2c33c47f7, 0xf271cabd2343171e, 0xbe5441dc4f0fba70, + 0x11183253702639d6, 0x01e0573a97074632, 0x268025dfce4f92d6, 0x47d14b05039a6f5b, + 0x000000000000015d, + ], + [ + 0x0b7a0b9ebb82fc5d, 0x7f2e7af14ff667a2, 0x5cc3df3907a17aec, 0x91465af2398fb92a, + 0x785b749098cf002e, 0xfe22218f7380cfb0, 0x32bd17997f4f03a8, 0x1c78bd791209f39a, + 0x00000000000000e4, + ], + [ + 0x5658a9eb87400032, 0xb3513556ff0b51af, 0x99ecda719408ac7b, 0x5e8d8b317bc54d5f, + 0x8f178f9e3ef10515, 0xfc49ad94624b9cf0, 0x344433fd8fc6c90f, 0x0df7be427f17965f, + 0x00000000000000d4, + ], + [ + 0x5ba4b10081d6751d, 0xeb0319dcb96a5495, 0x56407c25654729d3, 0x3de520199330d3fc, + 0xb3f8a00e28998bc4, 0xeeea220977ddd518, 0xc1429a49cb758f5e, 0xa86982a271834361, + 0x0000000000000040, + ], + [ + 0xebc08efd502a4e76, 0x4589d863767c0f52, 0xcf85eef44eb92dec, 0x0b59aa5d965c95c1, + 0x60945a8e68d2dbca, 0xc94cb99f77bf02d9, 0x6cc41e81cfc0dc2d, 0x7264e2dc8637cc2b, + 0x00000000000000d5, + ], + [ + 0x2cd49af1f76d4173, 0x269b00cea89acef1, 0xb60e2977a015990b, 0xbe86499685f72cef, + 0x4e23b9db4d5b0156, 0xfa92c71c6bb926ba, 0xf4a8ff2fe2f815b2, 0x543eee4aa5cd2e72, + 0x000000000000003a, + ], + [ + 0x036735d2ff9ed76d, 0x8653e8e35d154fca, 0x6811cba29824f792, 0x96a3c594e1252e06, + 0xb02543e7ba6e82e0, 0x5435187349b46518, 0xb7c14c8662bc5b59, 0x6535a2bba295f29b, + 0x00000000000000bf, + ], + [ + 0x042191fad75cf083, 0xaffc08409bd72c49, 0x8f5b9e3f67faf055, 0xa99fa2dfc9794f77, + 0x1ccbe885a9ef6764, 0x5fd1e3d66880eb3c, 0xbef622be06fd7bfa, 0x80d418fe9b10c7f8, + 0x00000000000001a9, + ], + [ + 0x8e846df2fa327b1c, 0x6755220d1d0321a0, 0xbacbc29c0342f918, 0xbe6f119dd996a1f8, + 0x11c02d40eb5740ff, 0x28a8232f4fbb962f, 0x066250a52185293a, 0xf9220c87cf927dc9, + 0x00000000000001c9, + ], + [ + 0xd63c86d1774512bf, 0x1524c2d8624719e4, 0x2f7568d486d00e48, 0x02140d74cb9cf033, + 0x3fff92dd0cae0cce, 0x7ec6068613941889, 0x791633ac5b8bf2b0, 0x43c98854ca54ea3d, + 0x0000000000000027, + ], + [ + 0xc8bc017afba4114a, 0x8e2791028d7327cd, 0xb95bfa6bddf662a4, 0xd828f02c44a2caef, + 0xc6504c42a27af13c, 0x67f79cab503e832d, 0x42da6976cb78ed1d, 0xda6e4727431e01db, + 0x0000000000000094, + ], + [ + 0x407a70134deacc7c, 0xa2540e3c94faf272, 0x4246beb934b43fa8, 0x157b03f43ba9f184, + 0xf7898d60b939ba99, 0x0f7e245e3a5f62b4, 0x2db5a341eaf7a575, 0xd3adea091278c0f7, + 0x00000000000000d2, + ], + [ + 0x6e60bc8cbebdd14b, 0xfe195cae8c57a662, 0x41e77bd8d9e4437a, 0x20fc2e7c85e4b9ed, + 0x97be566d2a012e02, 0x433786ae9b250c6a, 0x9eb76699cbb3224a, 0xf727091bf090f756, + 0x00000000000000c8, + ], + [ + 0xf29780090fd83acc, 0x247593df630645db, 0x171e6b9f9e2db252, 0x3a6342a3c30aec58, + 0x23524040fdd7151c, 0x2589b6988b15fee7, 0xca588f0a17dca917, 0xb2cf2fe7677cad49, + 0x00000000000001dc, + ], + [ + 0x5a0e940caa2f106a, 0x7c55eb23418781b7, 0xcc6cebb679cc53b8, 0x7f6f64fc9d7377f4, + 0x1571161c6de3403c, 0xd7a52f1cff1bf45a, 0xaf938df4110b0e06, 0x9bf219246f64c558, + 0x000000000000001c, + ], + [ + 0x3340fb54f31cb1da, 0xc1e88317e1752360, 0x183ae7f8121667b1, 0x1f271fa2bfd7bae9, + 0xb31175b92b745d3d, 0x22250cc540b13e3e, 0x63aabbb70d01026f, 0xab63c0f1b428cd91, + 0x0000000000000018, + ], + [ + 0x0801a2cf5f0f872a, 0x67a587356ec8c90c, 0xf21e24aba0913e94, 0x985fc1703502bcb9, + 0x8552800450a05926, 0x65918c8f426e56f8, 0x382414dea5cba2ac, 0xc3e7a7e62874cb00, + 0x000000000000005f, + ], + [ + 0x24b42a1c35c6448e, 0x129a536e1f4e7b86, 0x7ca12db87e48d8f6, 0xa827acf3587d6577, + 0xfc62bf4bb1cce77e, 0x702ee5800f45a823, 0xb4989843cc7a73a3, 0x3d49a2326c0afb5b, + 0x00000000000000a6, + ], + [ + 0xea6e991125fdd488, 0x4ea556fb3d0183b0, 0xe8cfd7c274dc3ac2, 0xe20307a3ddf1bd8f, + 0xfc6684582ee9be7b, 0x64a60b7937782071, 0x612fc9c865acf2e3, 0xcbb8c60c0de59326, + 0x000000000000009c, + ], + [ + 0xe51f8aca27806255, 0xcb4da51a101b2dbc, 0x27b8a0dfe08b3553, 0x6e2a0d066ad56ac8, + 0xa751cbedec44486d, 0xf574538ec5b2ae67, 0x64e1e3c3827536e2, 0x652fa060707a8b39, + 0x0000000000000170, + ], + [ + 0x5c7f569ad5fd84e2, 0xf84d6e126fa4bb7c, 0x7b74629c82a1081d, 0x7935151d0522589e, + 0x94fb78055ccf889a, 0xec677309792f6bac, 0x42fbece8d880a0d8, 0xd97b4881b70be7f5, + 0x00000000000000e3, + ], + [ + 0xa18a458f1bb9a390, 0x916fc4b62062e5d6, 0x7040176be16db823, 0x25de966301177efe, + 0x2b679ddeb778335c, 0x6ecf1689862622eb, 0xa191ffb5cb8a74eb, 0x4cc26680019ff743, + 0x0000000000000047, + ], + [ + 0xc8cd8f5d80a2aa22, 0xb73c67a164793948, 0x55abac7fff49c73d, 0xb5bc3df0833e5481, + 0xf6ab8a42384a9287, 0xbea5929eee4f0be7, 0x2e6bf0f29619ddba, 0x224ce58ab2ddfcbe, + 0x00000000000001d4, + ], + [ + 0x140767d0bcd89552, 0x31548634f10fdf7f, 0x2c4cdccc210355fc, 0x4929888955704a76, + 0x7102e8820f1d3e5e, 0x4afc588292691f55, 0x4cff09ede9e34260, 0x49d3a32932cd76cc, + 0x0000000000000063, + ], + [ + 0xaed91dc9263f94d8, 0x40f3c1cd20289709, 0x74dc1e9b60b5c995, 0xf08cb2a07d5bb95b, + 0xbb6d3339e642d133, 0x3c3e5d14d2fcc05d, 0xcf159316403e4a78, 0xd84be52727c124fa, + 0x00000000000000d4, + ], + [ + 0x140ce283ea9f1213, 0x2326ce38af8d7bc8, 0x90d21012914ac8b5, 0x0b07cea5e255c0e1, + 0x93cc5e972dcccb43, 0xb89b14f2ee9885ef, 0xa35afc40dc56967c, 0x8125d749ee2e3671, + 0x0000000000000101, + ], + [ + 0x66e566e3d3a87a30, 0x325081762cc44c2f, 0xdcaf7c52b80871ee, 0xdad02ffbdd245945, + 0xb9e691ace956cdb7, 0xdfefe49fc62064e5, 0xc322f6978c8f2c6d, 0x68a5d7a8aa88f4ac, + 0x00000000000000de, + ], + [ + 0xb72859988df31c84, 0x0489bf6fcf37c1c3, 0x4ccd18d9e4c102d7, 0x8b1d6c45ac3e23c9, + 0x7b7d259a5744d781, 0xa0033649b4778129, 0x5c7414d99fbf3e2c, 0xfa78a7fe20774c25, + 0x0000000000000043, + ], + [ + 0x272883fb34c28cb6, 0x22b00e9e5e7d03a4, 0x23bbacb97f4602a2, 0x27564d96c248ed06, + 0x9b7e1ce6d1c5b544, 0xb3d77b2d71182e92, 0xb18e78aade9d46ab, 0xde48d9e12e69d74d, + 0x00000000000001fb, + ], +]; + +pub(crate) const COMB_TABLE_Y: [[u64; 9]; 64] = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0], + [ + 0x88be94769fd16650, 0x353c7086a272c240, 0xc550b9013fad0761, 0x97ee72995ef42640, + 0x17afbd17273e662c, 0x98f54449579b4468, 0x5c8a5fb42c7d1bd9, 0x39296a789a3bc004, + 0x0000000000000118, + ], + [ + 0x545642a062e36127, 0xc3e4c6f71c38e6f5, 0x1d0e02c41bf9d6c4, 0x8a7a6fb6483ba43d, + 0x7104aee9cf390b9c, 0x4acdccdf2578e2fb, 0xc17af6df73b3feb4, 0x2fb789d77977fa95, + 0x00000000000001d6, + ], + [ + 0xba3b00c422d2d58a, 0x06c351857b0e93d5, 0x78810249545fa63d, 0x1d72e7923a6be922, + 0xe38827919e285f6f, 0x3212ca84b33128ef, 0x41640a1022acf93d, 0xbf1819691f33c6e9, + 0x00000000000001be, + ], + [ + 0x0f96137f2c91f83a, 0x1f743fa449f15b33, 0xd43b3ec0ccb4c1b1, 0x895a3be3cfeea9b0, + 0x27c3b4959eefee4d, 0xf1b4212712e344ee, 0x023afa76ff929c1e, 0x3078097686d07325, + 0x0000000000000166, + ], + [ + 0xed22e7b467081afd, 0xda1c5f3c81df2676, 0x48f406480ad04de0, 0x8d7720d1f7391873, + 0x111178cb13448f78, 0x39087943ea49e233, 0x692f712c73d8d101, 0xca20ab46f4e7e521, + 0x000000000000005a, + ], + [ + 0xcd2d1f04e4d1ae24, 0x099afd68e44bae9a, 0x669b9ba3a4880b2a, 0x5608536cab513fcb, + 0xfd7de6a330d8ace2, 0xebc44b8deb95e59e, 0x2302405e64969883, 0xbea96ff09b276f35, + 0x00000000000001b6, + ], + [ + 0xf69575cebe45049d, 0x418d09354958dacc, 0x142da6f236c0f076, 0x51d23cfba83dfce7, + 0x2df0421809714d01, 0x1260496a9ecec18d, 0xb4317aa1f700b1ad, 0x86174a20cd86c341, + 0x000000000000016c, + ], + [ + 0xce5bb75ea7b6bb9e, 0x9090358d569c9edd, 0xa8b5ec369cd0c065, 0xb2b5ac1cf81c82b8, + 0x8feb364c3ffceb86, 0x355ed9ecdf4f9f7e, 0x6f2efe425ce12ff4, 0xb23168780155070d, + 0x0000000000000035, + ], + [ + 0x5103486e84926700, 0x1c6b82b28fef959e, 0xb309fe2355b9fa8d, 0xdb35d40e4b8579ef, + 0xad5288e08145279a, 0x92b15dbc4ba26317, 0xda42d33ed9838c28, 0xd13ae9ba313a0399, + 0x0000000000000047, + ], + [ + 0x98dd1588d9de1e07, 0xbe9ed1154f6dc491, 0xd4605724f2b85d2b, 0xf94da3ba93aa2d3f, + 0x81229b618d489608, 0xb7e6a31ed9eaf52c, 0x787d526dab03e098, 0xc16ee426e5b06c1b, + 0x0000000000000171, + ], + [ + 0x87101fab8e15a6b9, 0xf5ad21a11a270874, 0x5d9a90506e900902, 0x15a2f5a1d7fe9a86, + 0x7ba4fb21dce37de8, 0x7751df45ff07bfb9, 0x12089b6202345c0b, 0x46f675ec6a9ace86, + 0x000000000000006d, + ], + [ + 0x582b79258c0a1261, 0x859c8a11788ba98e, 0x693c1e149a3ad4f7, 0xd3f4b8032d62063c, + 0x6affccfa0ddc5607, 0x63a3cd821eb6c797, 0xa94ce4ae6fb0c290, 0xce9479c1612cf5d1, + 0x0000000000000190, + ], + [ + 0xf9f79e729bc88c1e, 0x8459190a77e9ceb8, 0xdcb44b726a41a19d, 0xe30dbe8ce243350b, + 0xa168a6a9f3adebc1, 0x421fdb0c48e22f81, 0x3cc1d22a9b601ed1, 0x226ef6f6d2b564c0, + 0x0000000000000170, + ], + [ + 0x12d645e1fe536313, 0x14f4969e373af74b, 0xa56f032a30d498cc, 0xf30e4fe50a666b8c, + 0xfb42284e3fb31a5e, 0x065215a7efba6b77, 0x217ca76c6fa31b46, 0x13a6f6f91fc38bfd, + 0x0000000000000007, + ], + [ + 0xc4f3956b8e0c791b, 0x68cf3903875c10f4, 0x3ff4a407c1f7f0ad, 0x3672c4658902e832, + 0xdf5d0b6796ea88b4, 0x68df74bda86075a8, 0xe6d2d0675071e56e, 0x8c7fb077c7e5fcd5, + 0x00000000000001f1, + ], + [ + 0xe1b3574a7acb23ca, 0xadda7c9e3636a1c2, 0xfc998cf89f3d947c, 0x38ee4df41ba0511b, + 0x1f40cc1403e4694b, 0xccecf4e0800fb6d9, 0x021f708ae1665d06, 0x2bcd7975c492d329, + 0x0000000000000063, + ], + [ + 0xe62c76b958e8181c, 0x3731bcc7fee454ed, 0x2cb0b570ee89f8f5, 0x0a38b9539a262283, + 0x46887e748c19a774, 0x1dbea5dae80adab4, 0x09fb720cb1f75c90, 0x91e0b746e723e278, + 0x00000000000001c5, + ], + [ + 0x3da709346a145377, 0x1fb17f7fd16e2b7c, 0xef6432fcc0469077, 0x62a59a97d6660f5e, + 0x8ddc75505e2a0275, 0x2e65b1f75b88dce7, 0x0f4283da066f2c44, 0x561b9e15ce4b84a2, + 0x00000000000000ce, + ], + [ + 0x330d4d1d79410972, 0x7e6ef2a0ba1831bb, 0xa18cc9af4f4d5b93, 0x29d8de68069c9948, + 0xb8bc009db5e104a0, 0x4237d3728d929561, 0xa126a4e8a2274bab, 0x85ba9fcba99c454f, + 0x00000000000000cb, + ], + [ + 0x9e0d2f66306787cb, 0xe2e3c834fce146f5, 0x4b62143f2fdbe2b4, 0x19dab7745acb5238, + 0xee46bfcd7e138359, 0x29dbfa6675966b0f, 0xcde19c032d601936, 0xf7daf7b4f2aa4f6f, + 0x000000000000019c, + ], + [ + 0xb5e391e2b9b22600, 0xd20b3d088454c979, 0x4c9945642f77a85d, 0x972896db1f169687, + 0xd3554fa2ff8d2b83, 0x57123d99bd48e671, 0x1266b6b9da59843f, 0x633ff4df9241786d, + 0x000000000000011f, + ], + [ + 0x544a65284790d91f, 0xf71c90983f2924bd, 0xd5adb1d63e5c39e1, 0xeba20528d8cacd81, + 0xac72518ac728e735, 0x789a0bdf565cc9a0, 0x4b320c80c922bce2, 0x99b26c84e458f8ad, + 0x0000000000000138, + ], + [ + 0x7e2ede0bb08ffeec, 0xa37176acefbed279, 0x5acafeac0b72e855, 0x4c7c3ce3fcc75bad, + 0x2fc0939bb9a704a1, 0x6bf0c5328e4b4841, 0x5b72f1f2c10742b0, 0xaa62d2889c54371e, + 0x00000000000001c1, + ], + [ + 0xa0c846a9ed58b48c, 0xebe6013ae2590d05, 0x60c5d677da2fc481, 0xb6a0897b9252e895, + 0xb84595be218bf72d, 0xf1c527960dd1b9b1, 0x3f1583a86d38442a, 0xcda593dec4de5fdf, + 0x000000000000010a, + ], + [ + 0x789f22f80f551d73, 0x42ec3257e7ba5676, 0x3f52c4094cc69a65, 0xafdd277df46f1eb6, + 0xaead8379ec161194, 0xdc657d191f924e46, 0xe43263451578ff09, 0x4750da5878d091bf, + 0x0000000000000173, + ], + [ + 0x08f3f2a25182063f, 0xdea422b775992149, 0x845260b0d5cd158c, 0x6e0768e07c353c5a, + 0x5718a1c23afa81ca, 0x522f094d3ca2a17c, 0x96a361f77d93ebfd, 0x75c32db595c76942, + 0x0000000000000095, + ], + [ + 0x4ccb1f249e925079, 0x97af5a3dc5c916d3, 0x7ef650a8e497e318, 0x0d49927842975745, + 0xcf328ffb2462eba5, 0x8ee8e88f6e360cf3, 0xe15c3f8dc77cd3c2, 0x5ab87910736a646a, + 0x00000000000001cb, + ], + [ + 0x8e118540a87d4633, 0x3af88a0815200b8c, 0xca1806b488f790e1, 0x605032a919cd9554, + 0xf671627867d0cefd, 0xb58e8671d1676085, 0x51098dc308decf4c, 0x5a6cb932a50e312d, + 0x00000000000001fb, + ], + [ + 0xcde7b0d13de67115, 0xe645422dec23815b, 0x286703896b445b51, 0x11be335a80ae2784, + 0x9185d3df1a64b6e3, 0xa0ccbdd0ba228c67, 0x6bc79dff3a013cf7, 0x229f4e59b464c70c, + 0x0000000000000185, + ], + [ + 0xeb6045d192f826d9, 0xbd7c962e5facf02f, 0x87a5cfbd35fa1278, 0xfd1ff1cbdeda7159, + 0x1b0f6638a1287202, 0x861e7cbae1fcd5de, 0x33709b6bb9822a98, 0x171b15ba8b2ae55d, + 0x0000000000000130, + ], + [ + 0xa9d573cad9980812, 0x87d7913ea7e02cb9, 0xb772f40954b00468, 0x207850e8cc28eec8, + 0x772468db7603910d, 0xb5d3125db256e07a, 0xbeb567df8b59420d, 0xb3467a5331e5ccd1, + 0x00000000000001da, + ], + [ + 0x0a870cec12be5b78, 0x912c21c51406e140, 0x977417fdd5e13ca1, 0x9edbae8ab9e21e28, + 0x9fce45ea13b2f069, 0x25bd760b84f3cbf2, 0x29e0c3ce2489fd01, 0xe16d31c1ec62a766, + 0x0000000000000061, + ], + [ + 0x30df907b6bf9f426, 0xca534ca162ef821f, 0x540f8dba017699e6, 0x7cddfe567358f4af, + 0x5bc334a1e8f9cef6, 0x427fd83d6adc5f09, 0x2fd7c1b8b851f4c2, 0x234d1bd4d1587dae, + 0x00000000000000d3, + ], + [ + 0xb633df4e27c9f6a6, 0xdea08843cc406457, 0x75f688183cb011bc, 0x234d1b4ad5603044, + 0x2a156ae5b6a4d089, 0x9021af3b95e9f0f1, 0x90e987df968be7d3, 0x0821a4d3781ef8ab, + 0x0000000000000027, + ], + [ + 0x2051179030720125, 0xee2db45c6541fb74, 0x07bbcc41aacdcd66, 0xffa2e9630a08ced4, + 0x819b5b7a7f21a83d, 0x5f857f0a44896b10, 0x6126655c197cde7d, 0x81d0f14a2962acd7, + 0x0000000000000138, + ], + [ + 0x90389734e8b69225, 0x26dc02b5fc69c2f9, 0x37eeea93ff349339, 0x642556bfc05ec9e4, + 0xaa5744b64bd93ef1, 0xc29f3c91bfe9d602, 0x399cbf01aea4f871, 0xc02b2b1cf485b047, + 0x00000000000000be, + ], + [ + 0x47918d08133929fb, 0xd6cf0aa92ff979f6, 0xad55ab19a37a87ab, 0x9e9d1956c5010be8, + 0x8c0bd3ea3b82de19, 0x6c87820ff3c76b65, 0x70499d39f2232ed8, 0x70dab0ee7aafde31, + 0x0000000000000163, + ], + [ + 0x7472fd7ac36a0150, 0xfb61ab940138d406, 0x0cef5dd5b30829f5, 0xa8ea3257aef599f4, + 0x593011df7fa063bc, 0xea4c19ced774cb2c, 0xad0eeee6b85e8883, 0xb5ef126b2aba018b, + 0x000000000000014d, + ], + [ + 0x28989d2059b1302c, 0xba90dc20f89d0c77, 0xa29e9c3ee7f4a0b1, 0x332ecd54b0a225b5, + 0x38a3b080b042d9ea, 0x6d27ff408d9dfe9f, 0x146f8b3a88a69d3a, 0x634fb13f1fee39b6, + 0x0000000000000031, + ], + [ + 0x9cf75fb5ecb67b4b, 0x0132019091a51dbc, 0x0557fcd377225c2b, 0xe2e69807eec94860, + 0x58ccae9fe6c35624, 0xeca667a6e8c2ff7c, 0xf6939f23a37b64a3, 0x2eaabbb70d225b40, + 0x00000000000001e7, + ], + [ + 0xd8b41e4bb0625972, 0x1063ba69b0ce550d, 0x64687d34db86696e, 0xba2b364455ca06da, + 0xa4699ecd3dc4d46c, 0x9f40e631bfe4443f, 0x0e97fbe375fa4f7e, 0xd026b0481cbdc008, + 0x00000000000001d2, + ], + [ + 0x6637732018148fce, 0x9655f8b345c2a46f, 0xdc2952408caf763d, 0x4313d4b72816a721, + 0x6ff4aa0a50da95ed, 0x66cbcad2d4f9e551, 0x0ff11d35e6993438, 0x93dab445b9a4f5f1, + 0x00000000000000b4, + ], + [ + 0xd59cdf5b32a50e5a, 0xa13035882c379627, 0x0894c6ebff69b440, 0x881458aaad6055c9, + 0x27d9a7d5b2f2ff3c, 0x15f33160d254ae2d, 0x6e8342f1e38392bb, 0x7d7f4576b7c8d249, + 0x0000000000000104, + ], + [ + 0x15a79b14e40eb5e3, 0xccbbb5c75eeb2285, 0xc67c8dff5686232d, 0x8b45a2c1a83196ef, + 0xa274bf939b2acfb3, 0x35c2b5486e56b741, 0xc93963b24c1f5672, 0xbe9d6f8edf0e741b, + 0x00000000000001ca, + ], + [ + 0x44d5bd3eb9544fd3, 0x329d747f694035c9, 0x104d2b68676f065a, 0xc8e81e7cfc4165c6, + 0x4cdad2fdaf3779f2, 0xa3e4a4625669a833, 0xa281178a21737142, 0x86f7afb87b8f5514, + 0x00000000000001aa, + ], + [ + 0x2cba440ada687094, 0x3b478947767e75d2, 0x07c7de48078f23a0, 0xca6d86d8d7252108, + 0x5fd954fc6a7fbe1a, 0x4d552bbec9a2a901, 0x71ab270159e75804, 0x294298fa5413091c, + 0x0000000000000149, + ], + [ + 0x31496439ef08161c, 0x265407a997e3a008, 0x2088210700e2ffcd, 0x27426c055359fd16, + 0x03e836825c39f78f, 0x380743a8e2a8832e, 0x8739d8ed792bafe6, 0x6cef07c47a912015, + 0x0000000000000128, + ], + [ + 0x14332d38c8573626, 0xd4fe66fd4adedd4d, 0xc5d8f397bb83b706, 0x00aacee43aa8a4c9, + 0xe90383ff107c81a9, 0x6cf1fb2f18abdf47, 0x9bf5444df17c55ba, 0x8251ec253659322a, + 0x00000000000001de, + ], + [ + 0xe15e4f0ea5afb352, 0x9ff6c56df2ca770f, 0xe0e86c68338e1890, 0x96907f1990808bfc, + 0xd0d47de2686b805b, 0x2fbfcb72b367c12a, 0x800a58141dd3d54f, 0x2f4b07b398657a79, + 0x00000000000001f3, + ], + [ + 0x7435eaf9988d425a, 0xf9f323c7fcc441f4, 0x8de16b8c3e4de08e, 0x2e603853e495b0f0, + 0x204602204b3f0024, 0x8aff3f40b43cdb09, 0x409df7af4d00e185, 0xc681d091f1637f16, + 0x000000000000019f, + ], + [ + 0x96630e9e8fa87335, 0xdcf938c1c7771bc8, 0xc4f3d77aa8cf3cd8, 0x931ae9adc99a5fdd, + 0xa89581d55e2bcffc, 0xd0c7c71f29758819, 0x4ef995634d5aa9d8, 0xc6de91cd8f04cdeb, + 0x0000000000000171, + ], + [ + 0x7055c61255086dc5, 0x0622af5a3cf49868, 0x113dc4cbe695b064, 0x65b33365479ff727, + 0x156ab542336bc622, 0xf5a2ef703014770d, 0x486f74556ded88ee, 0xbb4619488c8a7452, + 0x000000000000005a, + ], + [ + 0x250e6112195cad69, 0x81cd7e491f0b4f39, 0x9b6808db61302e46, 0xe569c108b3af7841, + 0x34d86f57c15d5fc4, 0xb3e586fdf76ad338, 0x6de8477b70bd63a8, 0xa52b119d7ec86218, + 0x00000000000000b6, + ], + [ + 0xe17a85d7684c6e53, 0xc957477272d0d747, 0xf892866558d12edb, 0x7fb212c8c815db67, + 0x44b676eff3e66186, 0x15b57bb3998ef4d3, 0xb71f3b8e815c5036, 0x3fe9796573bcb190, + 0x0000000000000162, + ], + [ + 0x85d79261eda953e8, 0x35894e9b5dd94926, 0x95c1cbc99aeb12b8, 0x017029fe1695dc35, + 0x36e2b978d6b13364, 0xb86b7afdc388ce34, 0x2d25b1af1e501e40, 0xf9127abdd5d7e7c1, + 0x0000000000000031, + ], + [ + 0xfa5b847e047d9854, 0x6151153b6527d4d7, 0xf72721185bdda4c9, 0xdde383c5f17c7e63, + 0xc44040392a82edb4, 0xd1355d9781853b97, 0xcb503386b211c3b0, 0x0f9dbd602c17c154, + 0x0000000000000107, + ], + [ + 0x12f87e99ac4906b7, 0xe752f6ea5101c876, 0xa32a9b1f2b668943, 0x9e6e6c2b07950fdf, + 0x72a31f91bbb4c324, 0x2e799c7b28546e5f, 0x7183a2daa6ff4503, 0xe2cb8237178fdeeb, + 0x0000000000000030, + ], + [ + 0x6adbd32efbdc0c1e, 0xe3fe998b0ebefe86, 0x78099d79bc2a7085, 0x333c30095adeab44, + 0x45e4d96f07ae4b5f, 0x4bd1f0d29de88636, 0x8c7e69d7bf2020a9, 0x126a5886c3c5a340, + 0x0000000000000000, + ], + [ + 0xbb2246c1bcd8501d, 0x86d450b3a1600ead, 0x6f5a476db626b180, 0x01f4b93e3511ac45, + 0xd813396bbbfa9758, 0xc69b8e7512347b15, 0x108defb629cd9e2c, 0x4778bb2725e9ac47, + 0x000000000000012f, + ], + [ + 0x1855e97a55902052, 0x8a572e69e6f0db6c, 0xb1bb6014fcfa3f3c, 0xe77eb435f5b2440b, + 0x9045dd302df3f74a, 0x7853068b590ee6e0, 0x5714e1f03b9ddec5, 0x9810d77586086f6b, + 0x0000000000000008, + ], + [ + 0xee5555b183ae6c21, 0x895c837c478467b0, 0xe6b06030dd4d105b, 0x9d9f32247636a759, + 0x68132ea4fb8142dd, 0x6109a099ff919487, 0x775c37af76a1835d, 0x4279a9c48604a81e, + 0x0000000000000093, + ], + [ + 0x9b46685f8e69fd50, 0x2632cf3c3e47d4de, 0x44fd8f9f0238fed0, 0x2fbc3f42e03f169d, + 0x0c9df28eb7dcd132, 0x5deae7cc3d18592a, 0x3034fb93406adf0b, 0xbf5a3a75ae059280, + 0x000000000000010a, + ], + [ + 0x8de62222099effaf, 0x2212621b1328146c, 0x05f3c0b003677fcc, 0xf43e4825fb0fc3c0, + 0x94d3b33698536e0b, 0x22c1cca4225481eb, 0x2b8668dfa9fcbaf5, 0x51e858f2c30e9271, + 0x00000000000001e9, + ], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::p521::P521FieldElement; + use crate::p521_point::P521JacobianPoint; + + const G_X: [u64; 9] = [ + 0xf97e7e31c2e5bd66, 0x3348b3c1856a429b, 0xfe1dc127a2ffa8de, 0xa14b5e77efe75928, + 0xf828af606b4d3dba, 0x9c648139053fb521, 0x9e3ecb662395b442, 0x858e06b70404e9cd, + 0x00000000000000c6, + ]; + const G_Y: [u64; 9] = [ + 0x88be94769fd16650, 0x353c7086a272c240, 0xc550b9013fad0761, 0x97ee72995ef42640, + 0x17afbd17273e662c, 0x98f54449579b4468, 0x5c8a5fb42c7d1bd9, 0x39296a789a3bc004, + 0x0000000000000118, + ]; + + const WIDTH: usize = 6; + const BITS: usize = 521; + const D: usize = BITS.div_ceil(WIDTH); + const TABLE_SIZE: usize = 1 << WIDTH; + + /// Regenerates the comb table from scratch via the crate's own (independently verified) + /// point arithmetic. See [`crate::p256_comb_table`]'s identical test for the construction. + #[test] + fn regenerated_table_matches_checked_in_constants() { + let g = P521JacobianPoint::from_affine( + P521FieldElement::from_limbs(G_X), + P521FieldElement::from_limbs(G_Y), + ); + + let mut pow2 = [g; WIDTH]; + for i in 1..WIDTH { + let mut p = pow2[i - 1]; + for _ in 0..D { + p = p.double(); + } + pow2[i] = p; + } + + let mut table = [P521JacobianPoint::INFINITY; TABLE_SIZE]; + for bit in (0..WIDTH).rev() { + let step = 1usize << bit; + let pw = pow2[bit]; + let mut i = step; + while i < TABLE_SIZE { + table[i] = table[i - step].add(&pw); + i += 2 * step; + } + } + + for (idx, point) in table.iter().enumerate() { + if idx == 0 { + assert!(point.is_infinity().to_bool(), "table[0] must be infinity"); + assert_eq!(COMB_TABLE_X[0], [0, 0, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!(COMB_TABLE_Y[0], [0, 0, 0, 0, 0, 0, 0, 0, 0]); + } else { + let (x, y) = point + .to_affine() + .unwrap_or_else(|| panic!("regenerated table[{idx}] is unexpectedly infinity")); + assert_eq!(x.to_limbs(), COMB_TABLE_X[idx], "table[{idx}].x mismatch"); + assert_eq!(y.to_limbs(), COMB_TABLE_Y[idx], "table[{idx}].y mismatch"); + } + } + } +} diff --git a/crypto/ec/src/p521_domain.rs b/crypto/ec/src/p521_domain.rs new file mode 100644 index 000000000..0811088cd --- /dev/null +++ b/crypto/ec/src/p521_domain.rs @@ -0,0 +1,25 @@ +//! P-521 domain parameters, NIST SP 800-186 (Feb 2023) §3.2.1.5. `p` and `a = -3` live in +//! [`crate::p521`] (the `a = -3` shortcut is baked directly into +//! [`crate::p521_point::P521JacobianPoint::double`]); `n` lives in [`crate::p521_scalar`]. This +//! module holds the rest: the curve coefficient `b` and the base point `G`. + +/// The curve coefficient `b` in `y^2 = x^3 - 3x + b`, little-endian `u64` limbs. +pub const B_LIMBS: [u64; 9] = [ + 0xef451fd46b503f00, 0x3573df883d2c34f1, 0x1652c0bd3bb1bf07, 0x56193951ec7e937b, + 0xb8b489918ef109e1, 0xa2da725b99b315f3, 0x929a21a0b68540ee, 0x953eb9618e1c9a1f, + 0x0000000000000051, +]; + +/// The base point `G`'s `x` coordinate, little-endian `u64` limbs. +pub const G_X_LIMBS: [u64; 9] = [ + 0xf97e7e31c2e5bd66, 0x3348b3c1856a429b, 0xfe1dc127a2ffa8de, 0xa14b5e77efe75928, + 0xf828af606b4d3dba, 0x9c648139053fb521, 0x9e3ecb662395b442, 0x858e06b70404e9cd, + 0x00000000000000c6, +]; + +/// The base point `G`'s `y` coordinate, little-endian `u64` limbs. +pub const G_Y_LIMBS: [u64; 9] = [ + 0x88be94769fd16650, 0x353c7086a272c240, 0xc550b9013fad0761, 0x97ee72995ef42640, + 0x17afbd17273e662c, 0x98f54449579b4468, 0x5c8a5fb42c7d1bd9, 0x39296a789a3bc004, + 0x0000000000000118, +]; diff --git a/crypto/ec/src/p521_extra_bits.rs b/crypto/ec/src/p521_extra_bits.rs new file mode 100644 index 000000000..343f5366c --- /dev/null +++ b/crypto/ec/src/p521_extra_bits.rs @@ -0,0 +1,87 @@ +//! FIPS 186-5 Appendix A.4.1, "Conversion of a Bit String to an Integer mod n via Modular +//! Reduction" -- the "extra random bits" method `bouncycastle-ecdsa`'s `keys_p521` key generation +//! (Appendix A.2.1) and `ecdsa_p521` randomised per-message secret (Appendix A.3.1) use to turn +//! a DRBG output wider than `n` into a value in `[1, n-1]`. Identical algorithm to +//! [`crate::p256_extra_bits`] -- see that module's docs for the full reasoning -- with P-521's own +//! `n` substituted and, because of `n`'s width, its own reduction (see +//! [`reduce_wide_bits_mod_n_minus_1`]). +//! +//! # Why P-521 draws extra bits at all +//! +//! Not for bias: FIPS 186-5 Table A.2 gives `p521` a required and recommended DRBG output of 521 +//! bits for key generation, since `n` is within `2^260` of `2^521`. But Appendix A.3.1 requires +//! `N + t` bits with `t >= 64` for the per-message secret regardless, so `bouncycastle-ecdsa` +//! draws `N + 71 = 592` bits (74 bytes, the byte-aligned width at or above `585`) for both and +//! reduces them here. `p521_extra_bits_tests.rs` pins known answers computed in Python. + +use crate::nat; +use crate::p521_scalar::{N_LIMBS, P521Scalar}; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// `n - 1`, little-endian `u64` limbs. `N_LIMBS[0]` is odd (`n` is prime), so the subtraction +/// never borrows out of the low limb. +const N_MINUS_1_LIMBS: [u64; 9] = [ + N_LIMBS[0] - 1, + N_LIMBS[1], + N_LIMBS[2], + N_LIMBS[3], + N_LIMBS[4], + N_LIMBS[5], + N_LIMBS[6], + N_LIMBS[7], + N_LIMBS[8], +]; + +/// `2^521 mod (n-1)`, the fold constant [`reduce_wide_bits_mod_n_minus_1`] uses: `n - 1` has +/// no special form, so this is a genuine ~259-bit value, computed in Python from SP 800-186 +/// §3.2.1.5's `n`. +const TWO_POW_521_MOD_N_MINUS_1_LIMBS: [u64; 9] = [ + 0x449048e16ec79bf8, 0xc44a36477663b851, 0x8033feb708f65a2f, 0xae79787c40d06994, + 0x0000000000000005, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, +]; + +/// FIPS 186-5 Appendix A.4.1 steps 3-5 for P-521's `n`: reduces the big-endian bit string `bytes` +/// (up to 80 bytes; the DRBG draw is 74) modulo `n-1` and adds `1`, landing in `[1, n-1]`. +/// +/// P-521's `n - 1` is `2^521`-ish while the limb width is `2^576`, so neither the Solinas fold +/// the field uses nor [`crate::barrett`] (which wants a modulus filling its top limb) +/// applies as written. What does is a single fold at bit 521: split `x = hi * 2^521 + lo`, so +/// `x = hi * (2^521 mod (n-1)) + lo (mod n-1)`. With `x < 2^640`, `hi < 2^119` and the fold +/// constant is below `2^259`, so `hi * C + lo < 2^521 + 2^378 < 2 (n-1)`, and one conditional +/// subtraction finishes the reduction. Every step is branch-free in `x`: a fixed-width +/// multiplication, an addition and a masked select. Verified (not checked in) in Python against +/// arbitrary-precision `%` over 20,000 pseudorandom inputs at widths up to 80 bytes plus the +/// all-ones extremes. +pub fn reduce_wide_bits_mod_n_minus_1(bytes: &[u8]) -> P521Scalar { + assert!( + bytes.len() <= 80, + "reduce_wide_bits_mod_n_minus_1 given {} bytes, more than 80", + bytes.len() + ); + let x = crate::barrett::limbs_from_be_bytes::<10>(bytes); + + // lo = x mod 2^521: the low 8 limbs plus the low 9 bits of limb 8. + let mut lo = [0u64; 9]; + lo[..8].copy_from_slice(&x[..8]); + lo[8] = x[8] & 0x1ff; + // hi = x >> 521: bits 9..63 of limb 8 and all of limb 9, at most 119 bits. + let mut hi = [0u64; 9]; + hi[0] = (x[8] >> 9) | (x[9] << 55); + hi[1] = x[9] >> 9; + + let product = crate::montgomery::widening_mul::<9, 18>(&hi, &TWO_POW_521_MOD_N_MINUS_1_LIMBS); + debug_assert!(product[9..].iter().all(|&limb| limb == 0), "hi * C must fit in 9 limbs"); + let mut folded = [0u64; 9]; + folded.copy_from_slice(&product[..9]); + let (sum, carry) = nat::add(&folded, &lo); + debug_assert_eq!(carry, 0, "the fold's sum must fit in 9 limbs"); + + let (reduced, borrow) = nat::sub(&sum, &N_MINUS_1_LIMBS); + let mut acc = [0u64; 9]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &reduced, &mut acc); + + let (plus_one, _) = nat::add(&acc, &[1, 0, 0, 0, 0, 0, 0, 0, 0]); + P521Scalar::from_limbs(plus_one) +} diff --git a/crypto/ec/src/p521_mul.rs b/crypto/ec/src/p521_mul.rs new file mode 100644 index 000000000..483502856 --- /dev/null +++ b/crypto/ec/src/p521_mul.rs @@ -0,0 +1,97 @@ +//! Constant-time variable-base scalar multiplication: `[k]Q` for a secret `k` and a point `Q` +//! that is not the base point -- a peer's public key, for the ECC CDH primitive of SP 800-56A +//! Rev. 3 Section 5.7.1.2 (`P = h d_A Q_B`). +//! +//! The crate's other two multipliers do not cover this case: [`crate::p521_comb`] is fixed-base +//! (`[k]G` from a compile-time table of multiples of `G`), and [`crate::p521_wnaf`] takes +//! [`crate::p521_scalar::P521PublicScalar`] and is variable-time by design. This one takes +//! [`P521Scalar`], the secret type, and is branch-free in `k` throughout. +//! +//! # Fixed window, width 4 +//! +//! `[k]Q` is computed left to right over `ROUNDS = ceil(BITS / 4)` windows of 4 bits: each round +//! doubles the accumulator four times and adds `[w]Q` for the round's window value `w`, read off +//! `k`'s limbs at positions fixed by the loop (so *which* bits are read is public, only their +//! values are secret). `[w]Q` comes from a table of `[0]Q .. [15]Q` built per call in Jacobian +//! coordinates -- entry 0 is the identity, the rest by doubling and adding `Q` -- and is read by +//! `table_lookup`, which scans every entry under a mask rather than indexing by `w`, as the +//! comb's lookup does. The accumulator's addition is the complete [`P521JacobianPoint::add`]: it +//! masks in the identity, same-point and opposite-point cases, so the accumulator may be the +//! identity (it starts there and stays there through leading zero windows), a table entry may be +//! the identity (`w = 0`), and `Q` itself may be added to a multiple of `Q`; the comb's "no +//! offset" argument carries over unchanged. +//! +//! Cost: `4 * ROUNDS` doublings, `ROUNDS` full additions, `ROUNDS` masked scans of the table and +//! 14 point operations to build it: on P-256 about three times the fixed-base comb (whose table +//! is precomputed and whose additions are mixed) and 1.3 times the variable-time wNAF multiplier +//! (136 us against 44 us and 106 us in `p256_point_benches` / `p256_scalar_benches`). The table +//! is `16 * size_of::()` bytes of stack. Width 4 over width 5 keeps +//! that table at 1.5 KiB here and 3.4 KiB on P-521 for one fewer addition in five; a Montgomery +//! ladder would need an addition per bit. The table and the working accumulator are scrubbed +//! before returning; the caller owns the result and scrubs it when it is done. +//! +//! Verified against [`crate::p521_comb`] (for `Q = G`), against [`crate::p521_wnaf`] and against +//! `[a k mod n]G` (for `Q = [a]G`) in `p521_mul_tests.rs`, including `k = 0`, `k = 1`, `k = n-1`, +//! `Q` the identity, `Q` negated and `Q` in a non-canonical-`Z` representation. + +use crate::p521_point::{P521JacobianPoint, select_point}; +use crate::p521_scalar::P521Scalar; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 4; +const BITS: usize = 521; +const ROUNDS: usize = BITS.div_ceil(WIDTH); +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]Q`, branch-free in `k`. `Q` may be any point, including the identity; `k` may be any +/// value in `[0, n-1]`, and `[0]Q` is the identity. +pub fn multiply_point(k: &P521Scalar, q: &P521JacobianPoint) -> P521JacobianPoint { + let mut table = [P521JacobianPoint::INFINITY; TABLE_SIZE]; + table[1] = *q; + for i in 2..TABLE_SIZE { + // Which entry is built how depends only on the (public) index, never on `k` or `Q`. + table[i] = if i % 2 == 0 { table[i / 2].double() } else { table[i - 1].add(q) }; + } + + let limbs = k.limbs(); + let mut r = P521JacobianPoint::INFINITY; + for round in 0..ROUNDS { + let low_bit = (ROUNDS - 1 - round) * WIDTH; + let window = window_at(limbs, low_bit); + for _ in 0..WIDTH { + r = r.double(); + } + let mut entry = table_lookup(&table, window); + r = r.add(&entry); + entry.zeroize(); + } + + for entry in table.iter_mut() { + entry.zeroize(); + } + r +} + +/// The `WIDTH` bits of `limbs` from bit `low_bit` up, as a window value in `0..TABLE_SIZE`. Bits +/// at or above `BITS` read as zero, so on a curve whose `BITS` is not a multiple of `WIDTH` +/// (P-521) the top window sees only the scalar. The bit positions are public; the shifts and +/// masks are branch-free in the limb values. +fn window_at(limbs: &[u64; 9], low_bit: usize) -> usize { + let mut window = 0usize; + for j in (low_bit..low_bit + WIDTH).rev() { + let bit = if j < BITS { (limbs[j / 64] >> (j % 64)) & 1 } else { 0 }; + window = (window << 1) | (bit as usize); + } + window +} + +/// Table entry `secret_index`, selected by scanning every entry under a mask so the index never +/// steers which memory is read. +fn table_lookup(table: &[P521JacobianPoint; TABLE_SIZE], secret_index: usize) -> P521JacobianPoint { + let mut r = P521JacobianPoint::INFINITY; + for (i, entry) in table.iter().enumerate() { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + r = select_point(is_this_entry, entry, &r); + } + r +} diff --git a/crypto/ec/src/p521_point.rs b/crypto/ec/src/p521_point.rs new file mode 100644 index 000000000..5c5b3ec6e --- /dev/null +++ b/crypto/ec/src/p521_point.rs @@ -0,0 +1,361 @@ +//! Branch-free Jacobian-coordinate point arithmetic on the P-521 curve `y^2 = x^3 - 3x + b`. +//! +//! Identical in shape to [`crate::p256_point`]/[`crate::p384_point`] -- see +//! [`crate::p256_point`]'s docs for the full derivation and verification methodology, which +//! applies unchanged here (P-521 also has `a = -3` and cofactor `h = 1` per SP 800-186 §3.2.1.5). + +use crate::p521::P521FieldElement; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// A point on the P-521 curve in Jacobian coordinates. +#[derive(Clone, Copy, Debug)] +pub struct P521JacobianPoint { + /// `X`, the Jacobian `X` coordinate. + pub x: P521FieldElement, + /// `Y`, the Jacobian `Y` coordinate. + pub y: P521FieldElement, + /// `Z`, the Jacobian `Z` coordinate; `Z == 0` denotes the point at infinity. + pub z: P521FieldElement, +} + +impl P521JacobianPoint { + /// The point at infinity, the identity of the curve's group. + pub const INFINITY: Self = + Self { x: P521FieldElement::ONE, y: P521FieldElement::ONE, z: P521FieldElement::ZERO }; + + /// Builds a Jacobian point from affine coordinates (`Z = 1`). Does not check that `(x, y)` is + /// actually on the curve -- see [`crate::p521_sec1`] for that. + pub fn from_affine(x: P521FieldElement, y: P521FieldElement) -> Self { + Self { x, y, z: P521FieldElement::ONE } + } + + /// TRUE iff this is the point at infinity. + pub fn is_infinity(&self) -> Condition { + self.z.is_zero() + } + + /// Converts to affine `(x, y)` coordinates, or `None` for the point at infinity. Not constant + /// time: see [`crate::p256_point::P256JacobianPoint::to_affine`]'s docs. + pub fn to_affine(&self) -> Option<(P521FieldElement, P521FieldElement)> { + if self.is_infinity().to_bool() { + return None; + } + let z_inv = self.z.invert(); + let z_inv2 = z_inv.mul(&z_inv); + let z_inv3 = z_inv2.mul(&z_inv); + Some((self.x.mul(&z_inv2), self.y.mul(&z_inv3))) + } + + /// `-self`: negating the `Y` coordinate. + pub fn negate(&self) -> Self { + Self { x: self.x, y: self.y.negate(), z: self.z } + } + + /// Overwrites all three coordinates with zero (which is not the identity's encoding), for + /// scrubbing a secret-derived point before it goes out of scope; see + /// [`P521FieldElement::zeroize`]. + pub fn zeroize(&mut self) { + self.x.zeroize(); + self.y.zeroize(); + self.z.zeroize(); + } + + /// `2 * self`, via the curve's `a = -3` Jacobian doubling shortcut. See + /// [`crate::p256_point::P256JacobianPoint::double`]'s docs. + pub fn double(&self) -> Self { + let y1_squared = self.y.mul(&self.y); + let t = y1_squared.mul(&y1_squared); + let z1_squared = self.z.mul(&self.z); + let t1 = self.x.sub(&z1_squared); + let m = self.x.add(&z1_squared); + let m = m.mul(&t1); + let m = m.add(&m).add(&m); + let s = y1_squared.mul(&self.x); + let s = s.add(&s).add(&s).add(&s); + let t8 = t.add(&t).add(&t).add(&t).add(&t).add(&t).add(&t).add(&t); + let x3 = m.mul(&m).sub(&s).sub(&s); + let y3 = s.sub(&x3); + let y3 = y3.mul(&m).sub(&t8); + let z3 = self.y.add(&self.y).mul(&self.z); + Self { x: x3, y: y3, z: z3 } + } + + /// The generic (`self != ±other`, neither infinite) Jacobian addition formula, plus the `H` + /// and `R` intermediates [`Self::add`] uses to detect the exceptional cases. See + /// [`crate::p256_point::P256JacobianPoint`]'s private `generic_add`'s docs. + fn generic_add(&self, other: &Self) -> (Self, P521FieldElement, P521FieldElement) { + let z2_sq = other.z.mul(&other.z); + let u1 = self.x.mul(&z2_sq); + let s1 = self.y.mul(&other.z).mul(&z2_sq); + let z1_sq = self.z.mul(&self.z); + let u2 = other.x.mul(&z1_sq); + let s2 = other.y.mul(&self.z).mul(&z1_sq); + + let h = u1.sub(&u2); + let r = s1.sub(&s2); + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); + let v = h_squared.mul(&u1); + let g_neg = g.negate(); + let acc = s1.mul(&g_neg); + let g2 = g_neg.add(&v).add(&v); + let x3 = r.mul(&r).sub(&g2); + let y3_temp = v.sub(&x3); + let y3 = acc.add(&y3_temp.mul(&r)); + let z3 = h.mul(&self.z).mul(&other.z); + + (Self { x: x3, y: y3, z: z3 }, h, r) + } + + /// `self + other`, branch-free per [`crate::p256_point::P256JacobianPoint::add`]'s docs. + pub fn add(&self, other: &Self) -> Self { + let self_is_infinity = self.is_infinity(); + let other_is_infinity = other.is_infinity(); + + let (generic, h, r) = self.generic_add(other); + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + // Dropping the `!` here (making `is_opposite_point` collapse onto `is_same_point`) is an + // accepted mutant, not a bug: when `h == 0`, `generic_add`'s own `z3 = h*self.z*other.z` + // is already `0` regardless of `r`, so `generic` is already infinity-representing in the + // true opposite-point case (`h == 0, r != 0`) even without this override selecting + // `Self::INFINITY` explicitly. + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for an `other` given in **affine** coordinates (`Z2 = 1`), or the identity + /// when `other_is_infinity` is TRUE (`x2`/`y2` are then ignored). The same group law and the + /// same branch-free discipline as [`Self::add`] -- generic-add and doubling candidates computed + /// unconditionally, the exceptional cases masked in -- but with `Z2 = 1` the generic formula + /// simplifies: `U1 = X1`, `S1 = Y1` and `Z3 = H * Z1`, which drops five of its sixteen field + /// multiplications. The fixed-base comb multiplier adds a table entry to its accumulator on + /// every round, and every table entry is affine or the identity, so it is the caller this + /// exists for; `pub(crate)` because a caller has to know its point really has `Z = 1`. + pub(crate) fn add_affine( + &self, + x2: &P521FieldElement, + y2: &P521FieldElement, + other_is_infinity: Condition, + ) -> Self { + let self_is_infinity = self.is_infinity(); + + // The generic mixed addition, computed unconditionally: `U1 = X1`, `S1 = Y1`. + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2 + let r = self.y.sub(&s2); // S1 - S2 + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); // R^2 + H^3 - 2V + let y3 = acc.add(&v.sub(&x3).mul(&r)); // -S1*H^3 + (V - X3)*R + let z3 = h.mul(&self.z); // H * Z1 * Z2 with Z2 = 1 + let generic = Self { x: x3, y: y3, z: z3 }; + + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + let other = Self { x: *x2, y: *y2, z: P521FieldElement::ONE }; + + // Same priority order as `add`: the identity cases override everything else. + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, &other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for points that are **public**, branching on the exceptional cases instead + /// of computing every candidate and masking. + /// + /// [`Self::add`] must evaluate the doubling candidate on every call, because on secret inputs + /// it cannot branch on whether the doubling case applies -- that costs a full point doubling + /// (roughly a third of the addition) on every addition, whether or not it is ever used. This + /// version pays it only when the operands really are the same point, which for a scalar + /// multiplier over distinct precomputed multiples is essentially never. + /// + /// Restricted to `pub(crate)` and used only by [`crate::p521_wnaf`], whose scalars are + /// [`crate::p521_scalar::P521PublicScalar`] and whose points are a signer's public key and + /// the curve's own base point: every value it branches on is already known to an attacker. Do + /// not call this on anything derived from a private key or a per-message secret -- the + /// constant-time [`Self::add`] exists for that, and the scalar type split is what keeps the + /// two multipliers from being confused for one another. + pub(crate) fn add_vartime(&self, other: &Self) -> Self { + if self.is_infinity().to_bool() { + return *other; + } + if other.is_infinity().to_bool() { + return *self; + } + + let (generic, h, r) = self.generic_add(other); + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + generic + } + /// `self + (x2, y2)` for a **public**, affine `other` (`Z2 = 1`): [`Self::add_vartime`]'s + /// early-exit structure over [`Self::add_affine`]'s mixed formula, for the variable-time + /// multiplier's fixed side, whose precomputed multiples of `G` are stored affine. + /// `pub(crate)` for both of the reasons its two parents are. + pub(crate) fn add_vartime_affine(&self, x2: &P521FieldElement, y2: &P521FieldElement) -> Self { + if self.is_infinity().to_bool() { + return Self::from_affine(*x2, *y2); + } + + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2, with U1 = X1 + let r = self.y.sub(&s2); // S1 - S2, with S1 = Y1 + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); + let y3 = acc.add(&v.sub(&x3).mul(&r)); + let z3 = h.mul(&self.z); // H * Z1, with Z2 = 1 + Self { x: x3, y: y3, z: z3 } + } +} + +pub(crate) fn select_point( + cond: Condition, + a: &P521JacobianPoint, + b: &P521JacobianPoint, +) -> P521JacobianPoint { + P521JacobianPoint { + x: P521FieldElement::from_internal_limbs(select_limbs( + cond, + &a.x.internal_limbs(), + &b.x.internal_limbs(), + )), + y: P521FieldElement::from_internal_limbs(select_limbs( + cond, + &a.y.internal_limbs(), + &b.y.internal_limbs(), + )), + z: P521FieldElement::from_internal_limbs(select_limbs( + cond, + &a.z.internal_limbs(), + &b.z.internal_limbs(), + )), + } +} + +fn select_limbs(cond: Condition, a: &[u64; 9], b: &[u64; 9]) -> [u64; 9] { + let mut out = [0u64; 9]; + ct::conditional_select(cond, a, b, &mut out); + out +} + +// `add_vartime` is `pub(crate)` -- deliberately unreachable from outside the crate, since calling +// it on a secret point would undo the constant-time discipline [`P521JacobianPoint::add`] exists +// for -- so no integration test can reach it. Its whole contract is that it computes the same group +// law as `add`, differing only in *how* it gets there, so that is what is pinned here: agreement on +// every exceptional case, including the ones a scalar multiplier over distinct precomputed +// multiples essentially never reaches by chance (a point added to itself, and a point added to its +// own negation). QUALITY_AND_STYLE's private-function carve-out applies. +#[cfg(test)] +mod tests { + use super::*; + use crate::p521_domain::{G_X_LIMBS, G_Y_LIMBS}; + + #[test] + fn add_affine_agrees_with_add_on_every_case() { + let g = P521JacobianPoint::from_affine( + P521FieldElement::from_limbs(G_X_LIMBS), + P521FieldElement::from_limbs(G_Y_LIMBS), + ); + // A non-canonical-Z representation of G as well: `add_affine` reads `self.z`, so `self` + // must not be assumed affine even though `other` is. + let z = P521FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = P521JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + let selves = [g, g.double(), g.negate(), P521JacobianPoint::INFINITY, scaled_g]; + let others = [g, g.double(), g.negate()]; + for a in selves { + for b in others { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_affine(&bx, &by, Condition::::FALSE).to_affine(), + a.add(&b).to_affine(), + "add_affine disagrees with add" + ); + } + // The identity flag must win regardless of the coordinates passed alongside it. + assert_eq!( + a.add_affine(&g.x, &g.y, Condition::::TRUE).to_affine(), + a.to_affine(), + "add_affine with the identity flag must return self" + ); + } + } + + #[test] + fn add_vartime_affine_agrees_with_add_on_every_case() { + let g = P521JacobianPoint::from_affine( + P521FieldElement::from_limbs(G_X_LIMBS), + P521FieldElement::from_limbs(G_Y_LIMBS), + ); + let z = P521FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = P521JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + for a in [g, g.double(), g.negate(), P521JacobianPoint::INFINITY, scaled_g] { + for b in [g, g.double(), g.negate()] { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_vartime_affine(&bx, &by).to_affine(), + a.add(&b).to_affine(), + "add_vartime_affine disagrees with add" + ); + } + } + } + + #[test] + fn add_vartime_agrees_with_add_on_every_case() { + let g = P521JacobianPoint::from_affine( + P521FieldElement::from_limbs(G_X_LIMBS), + P521FieldElement::from_limbs(G_Y_LIMBS), + ); + // G, 2G, -G and the identity cover all four of `add`'s exceptional cases pairwise, plus + // the ordinary one (e.g. G + 2G). + let points = [g, g.double(), g.negate(), P521JacobianPoint::INFINITY]; + for a in points { + for b in points { + assert_eq!( + a.add_vartime(&b).to_affine(), + a.add(&b).to_affine(), + "add_vartime disagrees with add" + ); + } + } + } +} diff --git a/crypto/ec/src/p521_scalar.rs b/crypto/ec/src/p521_scalar.rs new file mode 100644 index 000000000..083cc3b5f --- /dev/null +++ b/crypto/ec/src/p521_scalar.rs @@ -0,0 +1,517 @@ +//! The P-521 scalar field, arithmetic mod the curve order `n` (SP 800-186 §3.2.1.5). Identical in +//! shape to [`crate::p256_scalar`]/[`crate::p384_scalar`] -- Montgomery form (`R = 2^576`, this +//! type's full 9-limb width, not `n`'s 521 bits), the same SOS reduction, the same +//! `Secret`/plain scalar split -- with `n` swapped for P-521's; see [`crate::p256_scalar`]'s docs +//! for the full derivation. +//! +//! Unlike [`crate::p256_scalar::redc`] and [`crate::p384_scalar::redc`], `n` here is not close to +//! `R`: `n < 2^521`, some 55 bits short of `R = 2^576`, so `2n` cannot exceed `R` and REDC's +//! accumulator needs no extra bit of headroom beyond the 18-limb product width. Verified (not +//! checked in) against 30,000 random trials plus the `(n-1)*(n-1)` worst case that the +//! accumulator's top limb is always exactly `0` after the reduction rounds -- kept as a +//! `debug_assert_eq!` rather than removing the (unused) 19th limb, so a future change that makes +//! it false fails loudly rather than silently dropping a bit the way [`crate::p384::reduce`]'s +//! analogous but *wrong* assumption once did. +//! +//! That same 55-bit gap bites a different function, [`reduce_wide`] (used by every `from_limbs` in +//! this module): a raw `[u64; 9]` can hold values up to `2^576 - 1`, so it is not generally `< 2n` +//! the way a same-width raw limb array is for P-256/P-384's closer-to-`R` `n` -- see that +//! function's own docs for the fold this needs and the real bug an earlier, single-subtraction +//! version had. + +use crate::nat; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +/// `n`, the order of the P-521 base point `G`, little-endian `u64` limbs (SP 800-186 §3.2.1.5). +pub const N_LIMBS: [u64; 9] = [ + 0xbb6fb71e91386409, 0x3bb5c9b8899c47ae, 0x7fcc0148f709a5d0, 0x51868783bf2f966b, + 0xfffffffffffffffa, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, +]; + +/// `2^576 mod n`, also the Montgomery representation of `1`. +const R_LIMBS: [u64; 9] = [ + 0xfb80000000000000, 0x28a2482470b763cd, 0x17e2251b23bb31dc, 0xca4019ff5b847b2d, + 0x02d73cbc3e206834, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, +]; + +/// `R^2 mod n`, used to bring a plain value into Montgomery form. +const R_SQUARED_LIMBS: [u64; 9] = [ + 0x137cd04dcf15dd04, 0xf707badce5547ea3, 0x12a78d38794573ff, 0xd3721ef557f75e06, + 0xdd6e23d82e49c7db, 0xcff3d142b7756e3e, 0x5bcc6d61a8e567bc, 0x2d8e03d1492d0d45, + 0x000000000000003d, +]; + +/// `-n^-1 mod 2^64`, the Montgomery reduction constant. +const N_PRIME: u64 = 0x1d2f5ccd79a995c7; + +/// `n - 2`, little-endian `u64` limbs -- the public exponent [`P521ScalarField::invert`] raises +/// its base to. +const N_MINUS_2_LIMBS: [u64; 9] = [ + 0xbb6fb71e91386407, 0x3bb5c9b8899c47ae, 0x7fcc0148f709a5d0, 0x51868783bf2f966b, + 0xfffffffffffffffa, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, +]; + +/// `2^521 mod n`, the fold constant [`reduce_wide`] uses. Unlike [`crate::p521::P521FieldElement`], +/// whose modulus `p = 2^521 - 1` makes `2^521 mod p` trivially `1`, `n` has no such special form, +/// so this is a genuine ~259-bit value computed directly (not a small Solinas-style constant). +const TWO_POW_521_MOD_N_LIMBS: [u64; 9] = [ + 0x449048e16ec79bf7, 0xc44a36477663b851, 0x8033feb708f65a2f, 0xae79787c40d06994, 0x5, 0, 0, 0, 0, +]; + +/// An element of the P-521 scalar field, i.e. an integer mod `n`. Stored internally in Montgomery +/// form; see the module docs. +#[derive(Clone, Copy, Debug)] +pub struct P521ScalarField([u64; 9]); + +impl P521ScalarField { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0, 0, 0, 0, 0, 0]); + + /// The multiplicative identity. Its Montgomery form is `R mod n`, not the literal integer `1`. + pub const ONE: Self = Self(R_LIMBS); + + /// Builds a scalar-field element from little-endian `u64` limbs (an ordinary, non-Montgomery + /// value; any of the `2^576` values a raw `[u64; 9]` can represent, not just those `< 2n`), + /// reducing via [`reduce_wide`], then converting to Montgomery form. + pub fn from_limbs(limbs: [u64; 9]) -> Self { + let reduced = reduce_wide(limbs); + Self(redc(&widening_mul(&reduced, &R_SQUARED_LIMBS))) + } + + /// Returns the canonical little-endian `u64` limbs (an ordinary, non-Montgomery value), `< n`. + pub fn to_limbs(&self) -> [u64; 9] { + let mut wide = [0u64; 18]; + wide[..9].copy_from_slice(&self.0); + redc(&wide) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + nat::is_zero(&self.0) + } + + /// `self + other mod n`. Operates directly on the Montgomery-form limbs: no REDC needed. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = nat::add(&self.0, &other.0); + let (sum_plus_r, _) = nat::add(&sum, &R_LIMBS); + let (diff, borrow) = nat::sub(&sum, &N_LIMBS); + let mut when_no_carry = [0u64; 9]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 9]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_r, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod n`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = nat::sub(&self.0, &other.0); + let (corrected, _) = nat::add(&diff, &N_LIMBS); + let mut result = [0u64; 9]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod n`. + pub fn negate(&self) -> Self { + let (diff, _) = nat::sub(&N_LIMBS, &self.0); + let mut result = [0u64; 9]; + ct::conditional_select(self.is_zero(), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod n`, via Montgomery multiplication. + pub fn mul(&self, other: &Self) -> Self { + Self(redc(&widening_mul(&self.0, &other.0))) + } + + /// Overwrites this value with zero, through a write the compiler may not elide. + /// + /// For the signing path, where a value of this type holds `d`, `k` or `k^-1` and would + /// otherwise stay legible on the stack after the signature is returned. This type is `Copy` + /// and carries public values too (verification's `u`, `v`, `e`, `r`, `s`), so it is not + /// wrapped in [`bouncycastle_utils::secret::Secret`] -- that would cost its `Copy`, its `const + /// ZERO`/`ONE`, and a scrub on every intermediate including the public ones. The trade is that + /// scrubbing is the caller's job, on every path out of the function; see + /// `bouncycastle_ecdsa`'s `sign_with_k` for the intended use. + /// + /// This does not reach values the compiler kept in registers or spilled itself, and is + /// defence in depth rather than a guarantee. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`widening_square`]. + /// + /// Used by the exponentiation loops ([`Self::invert`] and friends), which call it hundreds of + /// times in a row with nothing else competing for registers. Deliberately *not* used by this + /// curve's point arithmetic, which squares by calling `mul` with equal arguments: that was + /// tried and measured on P-256, and despite `square` being ~9% cheaper than `mul` in + /// isolation it made the constant-time comb multiplier consistently slower, because inlining + /// a third wide-multiply routine into that loop costs more than the saved partial products + /// return. See [`crate::p256::P256FieldElement::square`] for the numbers. + pub fn square(&self) -> Self { + Self(redc(&widening_square(&self.0))) + } + + /// Converts a secret scalar (`d` or `k`) into Montgomery form for field arithmetic. See + /// [`crate::p256_scalar::P256ScalarField::from_secret`]'s docs. + pub fn from_secret(secret: &P521Scalar) -> Self { + Self::from_limbs(*secret.limbs()) + } + + /// `self^-1 mod n`, or `0` if `self` is `0`. Fermat's little theorem (`n` is prime) over the + /// public exponent `n-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..9).rev() { + let limb = N_MINUS_2_LIMBS[limb_idx]; + // The top limb holds only 9 bits of the exponent, in nibbles 0..=2; its higher + // nibbles are zero and are skipped rather than spent on squarings of `1`. + let nibbles = if limb_idx == 8 { 3 } else { 16 }; + for nibble_idx in (0..nibbles).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } +} + +impl PartialEq for P521ScalarField { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. Comparing Montgomery-form limbs directly is valid: the map `x -> x*R mod n` is a + /// bijection. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..9 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for P521ScalarField {} + +/// Schoolbook widening multiply, identical in shape to [`crate::p256_scalar`]'s. +fn widening_mul(a: &[u64; 9], b: &[u64; 9]) -> [u64; 18] { + let mut result = [0u64; 18]; + for i in 0..9 { + let mut carry: u128 = 0; + for j in 0..9 { + let idx = i + j; + let prod = (a[i] as u128) * (b[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + // Mutating `carry = s >> 64` here is an accepted mutant, not a bug -- see + // `crate::p521::widening_mul`'s identical tail loop for the general argument (this loop's + // carry is always 0 or 1, and a left-shifted replacement contributes nothing to any later + // `result[k']`'s low 64 bits, so it never affects the returned `result` regardless of + // input). + let mut k = i + 9; + while k < 18 { + let s = (result[k] as u128) + carry; + result[k] = s as u64; + carry = s >> 64; + k += 1; + } + } + result +} + +/// Schoolbook squaring of a 9-limb value into its 18-limb square. +/// +/// `a * a` is symmetric: the product `a_i * a_j` appears twice for every `i != j`. Forming each of +/// those once and doubling costs `L(L+1)/2` limb multiplications -- 45 at this width against +/// the 81 [`widening_mul`] would form for the same value. +/// +/// Three passes: the off-diagonal products `a_i * a_j` for `i < j`; a doubling of the whole +/// accumulator; then the diagonal squares `a_i * a_i` added in at limb `2i`. Writing `result[i + +/// 9]` in the first pass is an assignment rather than an accumulation because row `i` only ever +/// reaches limbs `2i + 1 ..= i + 9 - 1`, and no earlier row reaches limb `i + 9` either, so that +/// limb is still zero when the row's final carry lands on it. The doubling cannot overflow: the +/// off-diagonal sum is strictly less than `a^2 / 2`. +/// +/// Verified against Python's arbitrary-precision `**2` over 20,000 random 9-limb values plus the +/// all-ones worst case before being written here. +fn widening_square(a: &[u64; 9]) -> [u64; 18] { + let mut result = [0u64; 18]; + + // Off-diagonal products, each formed once. + for i in 0..9 { + let mut carry: u128 = 0; + for j in (i + 1)..9 { + let idx = i + j; + let prod = (a[i] as u128) * (a[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + result[i + 9] = carry as u64; + } + + // Every off-diagonal product appears twice in the square. + let mut carry = 0u64; + for limb in result.iter_mut() { + let next_carry = *limb >> 63; + // `*limb << 1` always leaves bit 0 clear and `carry` is only ever 0 or 1, so the two + // operands are disjoint: mutating this `|` to `^` is an accepted equivalent, not a bug. + *limb = (*limb << 1) | carry; + carry = next_carry; + } + debug_assert_eq!(carry, 0, "doubling the off-diagonal sum overflowed 18 limbs"); + + // Diagonal squares. + let mut carry: u128 = 0; + for i in 0..9 { + let square = (a[i] as u128) * (a[i] as u128); + let low = (result[2 * i] as u128) + ((square as u64) as u128) + carry; + result[2 * i] = low as u64; + let high = (result[2 * i + 1] as u128) + (square >> 64) + (low >> 64); + result[2 * i + 1] = high as u64; + carry = high >> 64; + } + debug_assert_eq!(carry, 0, "adding the diagonal overflowed 18 limbs"); + + result +} + +/// `m * n`, one limb times the 9-limb modulus `n`, as the 10 limbs such a product can occupy. +/// +/// This is exactly what each round of [`redc`]'s SOS reduction needs, and the shape +/// [`crate::montgomery::redc`] has always used for the brainpool curves. Every hand-written scalar +/// REDC in this crate previously formed it with the general 9x9 [`widening_mul`] against a +/// zero-padded `[m, 0, ..., 0]` instead, asking for 81 limb multiplications where 9 are needed. +/// +/// Whether that padding costs anything turns out to depend entirely on the width, measured rather +/// than assumed: at four limbs the optimizer eliminates it completely (P-256 and secp256k1 time +/// identically either way), but it stops doing so by six, where the padded form measured 3.2x +/// slower per scalar multiplication for P-384 and 5.2x slower for P-521. All four are written this +/// way regardless, so the four modules read alike and none of them asks for work it does not want. +fn modulus_times_limb(m: u64) -> [u64; 10] { + let mut out = [0u64; 10]; + let mut carry: u128 = 0; + for j in 0..9 { + let prod = (m as u128) * (N_LIMBS[j] as u128) + carry; + out[j] = prod as u64; + carry = prod >> 64; + } + out[9] = carry as u64; + out +} + +/// Montgomery reduction (SOS method): given an 18-limb value `t < n*R`, returns `t * R^-1 mod n` +/// as 9 limbs. See [`crate::p256_scalar::redc`]'s docs for the algorithm. Unlike that function's +/// 9th limb (or [`crate::p384_scalar::redc`]'s 13th), the accumulator's 19th limb here is always +/// `0` (module docs), so it is not consulted after the reduction rounds beyond a debug assertion. +fn redc(t: &[u64; 18]) -> [u64; 9] { + let mut acc = [0u64; 19]; + acc[..18].copy_from_slice(t); + + for i in 0..9 { + let m = acc[i].wrapping_mul(N_PRIME); + let mn = modulus_times_limb(m); + + let mut carry: u128 = 0; + for j in 0..10 { + let idx = i + j; + let s = (acc[idx] as u128) + (mn[j] as u128) + carry; + acc[idx] = s as u64; + carry = s >> 64; + } + // Propagate the carry through every limb above the window, up to the accumulator's + // proven 19-limb bound. The trip count depends only on the public round index `i`, + // never on the operands: once the carry has propagated out, the remaining iterations + // add 0. A `while carry != 0` here would be a secret-dependent branch on the signing + // path. + for idx in (i + 10)..19 { + let s = (acc[idx] as u128) + carry; + acc[idx] = s as u64; + carry = s >> 64; + } + debug_assert_eq!(carry, 0, "carry must be fully propagated within the 19-limb bound"); + } + debug_assert_eq!(&acc[..9], &[0u64; 9], "REDC's low limbs must be cleared after 9 rounds"); + debug_assert_eq!(acc[18], 0, "P-521 REDC's 19th limb is always 0 -- see module docs"); + + let high: [u64; 9] = + [acc[9], acc[10], acc[11], acc[12], acc[13], acc[14], acc[15], acc[16], acc[17]]; + let (diff, borrow) = nat::sub(&high, &N_LIMBS); + let mut result = [0u64; 9]; + ct::conditional_select(Condition::::from_lsb(borrow), &high, &diff, &mut result); + result +} + +/// Reduces an arbitrary `[u64; 9]` value (any of the `2^576` values that type can hold, not just +/// those `< 2n`) to its canonical representative `< n`. +/// +/// Unlike P-256/P-384 (where `n` is close enough to `2^256`/`2^384` that a raw same-width limb +/// array is always `< 2n`, making a single conditional subtraction sufficient), P-521's `n` is +/// `2^521`-ish while this type's storage is a full `2^576`-wide `[u64; 9]` -- a 55-bit gap a caller +/// can easily fill (e.g. [`P521Scalar::from_be_bytes`]'s 66-byte/528-bit SEC 1 input, or simply any +/// `[u64; 9]` passed to `from_limbs`). An earlier version of this function assumed the gap couldn't +/// be filled and used a single conditional subtraction unconditionally; that assumption was false +/// (a raw `[u64; 9]` can represent values up to `2^576 - 1`), so it under-reduced any input `>= +/// 2n` -- caught not by this crate's own property tests (whose pseudorandom `[u64; 9]` values +/// happened to make the bug's output still self-consistent, see `p521_scalar_tests.rs`'s docs) but +/// by real ECDSA verification math in the wycheproof P-521 suite, where a legitimately-bounded +/// scalar's `redc` calls took a code path this crate's own tests never reached. +/// +/// The fix: split `limbs` into `hi = limbs >> 521` (the excess, `< 2^55`) and `lo = limbs mod +/// 2^521` (`< 2^521`), then fold via `value ≡ hi * (2^521 mod n) + lo (mod n)` +/// ([`TWO_POW_521_MOD_N_LIMBS`]). Verified (not checked in) against 300,000 random 9-limb trials +/// plus boundary cases (`n`, `n-1`, `2n-1`, all-ones, max-`hi`-with-max-`lo`) that the folded sum +/// is always `< 2n`, so the single conditional subtraction below remains valid -- the same +/// two-step shape as this crate's Solinas-prime field reductions, just with a genuine ~259-bit fold +/// constant instead of a small one, since `n` has no special algebraic form to exploit. +fn reduce_wide(limbs: [u64; 9]) -> [u64; 9] { + let hi = limbs[8] >> 9; + let mut lo = limbs; + lo[8] &= 0x1ff; + + let mut hi_limbs = [0u64; 9]; + hi_limbs[0] = hi; + let product = widening_mul(&hi_limbs, &TWO_POW_521_MOD_N_LIMBS); + let product_low: [u64; 9] = product[..9].try_into().unwrap(); + debug_assert_eq!( + &product[9..], + &[0u64; 9], + "hi * C must fit in 9 limbs (hi < 2^55, C < 2^259)" + ); + + let (folded, carry) = nat::add(&lo, &product_low); + debug_assert_eq!(carry, 0, "fold sum must fit in 9 limbs (proven < 2n < 2^522)"); + + let (diff, borrow) = nat::sub(&folded, &N_LIMBS); + let mut result = [0u64; 9]; + ct::conditional_select(Condition::::from_lsb(borrow), &folded, &diff, &mut result); + result +} + +/// A scalar mod `n`, `< n`, held in `Secret`. See [`crate::p256_scalar::P256Scalar`]'s docs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct P521Scalar(Secret<[u64; 9]>); + +impl P521Scalar { + /// Builds a secret scalar from little-endian `u64` limbs, reducing via [`reduce_wide`] (valid + /// for any of the `2^576` values a raw `[u64; 9]` can represent, not just those `< 2n`). + pub fn from_limbs(limbs: [u64; 9]) -> Self { + let mut secret = Secret::<[u64; 9]>::new(); + *secret = reduce_wide(limbs); + Self(secret) + } + + /// Builds a secret scalar from SEC 1 §2.3.7 big-endian octets, reducing via [`reduce_wide`] -- + /// needed here specifically: 66 bytes is 528 bits, 7 more than `n`'s 521, so this input is not + /// generally `< 2n` the way P-256/P-384's exactly-sized SEC 1 encodings are. + pub fn from_be_bytes(bytes: &[u8; 66]) -> Self { + Self::from_limbs(crate::p521_sec1::limbs_from_be_bytes(bytes)) + } + + /// Encodes to SEC 1 §2.3.7 big-endian octets. + pub fn to_be_bytes(&self) -> [u8; 66] { + crate::p521_sec1::be_bytes_from_limbs(&self.0) + } + + /// The scalar's limbs, for the crate's own multiplier and field-arithmetic implementations to + /// read. Not exposed outside the crate. + pub(crate) fn limbs(&self) -> &[u64; 9] { + &self.0 + } +} + +/// A scalar mod `n`, `< n`, held as a plain (non-secret) value. See +/// [`crate::p256_scalar::P256PublicScalar`]'s docs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct P521PublicScalar([u64; 9]); + +impl P521PublicScalar { + /// Builds a public scalar from little-endian `u64` limbs, reducing via [`reduce_wide`] (valid + /// for any of the `2^576` values a raw `[u64; 9]` can represent, not just those `< 2n`). + pub fn from_limbs(limbs: [u64; 9]) -> Self { + Self(reduce_wide(limbs)) + } + + /// Returns the scalar's little-endian `u64` limbs, `< n`. + pub fn to_limbs(&self) -> [u64; 9] { + self.0 + } + + /// `self^-1 mod n` in **variable time**, as a [`P521ScalarField`] ready for the arithmetic + /// that follows, or `0` for `0`. For ECDSA verification's `s` (FIPS 186-5 §6.4.2 step 4), + /// which is part of the signature and so already public; nothing derived from a private key + /// or a per-message secret may reach this, which is why it lives on the public scalar type + /// and not on [`P521ScalarField`], whose `invert` is the constant-time one. See + /// [`crate::inverse_vartime`]. + pub fn invert_vartime(&self) -> P521ScalarField { + P521ScalarField::from_limbs(crate::inverse_vartime::mod_inverse(&self.0, &N_LIMBS)) + } +} + +// `redc`'s carry propagation into the limbs above the `m * n` window (the `for idx in (i + 10)..19` +// tail above) runs unconditionally, but a non-zero carry actually reaching those limbs is rare -- +// verified, when that tail was still a `while carry != 0` loop, by instrumenting it with an +// unconditional `panic!` and confirming it fired during real signature verification (the +// wycheproof P-521 suite in `bouncycastle-ecdsa`) -- and this crate's own tests never happened to +// produce one: not `algebraic_identities_over_many_pseudorandom_values` +// (hundreds of thousands of `redc` calls via `invert`'s ~585 multiplications per iteration, over +// 1000 iterations, all with uniformly random `< n` operands), nor any of this module's other +// tests. `redc` is private, so this can't be pinned through the public API either; the +// QUALITY_AND_STYLE.md "private function, known-answer, can't be reached cleanly from outside the +// crate" exception applies. `T_LIMBS` is the exact 18-limb product that triggered the panic in the +// wycheproof run (captured by printing it, not hand-derived); `EXPECTED` was computed independently +// in Python two ways -- `(t * pow(R, -1, n)) % n` directly, and a line-by-line simulation of this +// same SOS algorithm -- which agreed with each other before either was compared to this crate's +// own output. +#[cfg(test)] +mod tests { + use super::*; + + const T_LIMBS: [u64; 18] = [ + 0x316b3dda1d070851, 0x69303a5b0ee10995, 0x3238669d5bd66d1a, 0x9828b88f2d9faf39, + 0xd229922b690684d0, 0x10ee65adb877b80b, 0x7fd5e3973e0f3364, 0xde220c48e8776f64, + 0x3a7e5d51b462c5ca, 0x4e0e624d9721269a, 0xa770b74d39cfd04c, 0x19b6118eac6cf99f, + 0xa30d0f077e5f161d, 0xfffffffffffffff4, 0xffffffffffffffff, 0xffffffffffffffff, + 0x000000000003ffff, 0x0000000000000000, + ]; + // Coincidentally equal to R_LIMBS (this input happened to arise from a step of Fermat + // inversion computing a value's final `a * a^(n-2) = a^(n-1) == 1`, i.e. `1`'s own Montgomery + // form) -- written out literally rather than as `R_LIMBS` so this stays a KAT against an + // independently computed value, not a reference to another constant in this file. + const EXPECTED: [u64; 9] = [ + 0xfb80000000000000, 0x28a2482470b763cd, 0x17e2251b23bb31dc, 0xca4019ff5b847b2d, + 0x02d73cbc3e206834, 0, 0, 0, 0, + ]; + + #[test] + fn redc_handles_the_rare_carry_propagation_case() { + assert_eq!(redc(&T_LIMBS), EXPECTED); + } +} diff --git a/crypto/ec/src/p521_sec1.rs b/crypto/ec/src/p521_sec1.rs new file mode 100644 index 000000000..144f4ab2b --- /dev/null +++ b/crypto/ec/src/p521_sec1.rs @@ -0,0 +1,212 @@ +//! SEC 1 v2.0 (Certicom, May 2009) §2.3.3/§2.3.4 point encoding and decoding for P-521, and the +//! public-key validation SP 800-186 (Feb 2023) Appendix D.1.1.1 requires. Identical in shape to +//! [`crate::p256_sec1`]/[`crate::p384_sec1`] -- see [`crate::p256_sec1`]'s docs for the full +//! reasoning -- with P-521's 66-byte field-element width (`ceil(521/8)`) and 9-limb type +//! substituted. `p` is `3 mod 4` here too (verified in this module's tests), so the same +//! compressed-decode square-root approach applies. +//! +//! Unlike P-256/P-384, `66 * 8 = 528` bits is 7 bits wider than `p`'s 521, so a valid encoded +//! field element's top byte always has its high 7 bits clear; [`limbs_less_than_p`] (via ordinary +//! big-integer comparison against [`P_LIMBS`]) already rejects any encoding that sets them, since +//! such a value is `>= p` (indeed `>= 2^521 > p`). + +use crate::nat; +use crate::p521::{P_LIMBS, P521FieldElement}; +use crate::p521_domain::B_LIMBS; +use crate::p521_point::P521JacobianPoint; + +/// `(p + 1) / 4`, the exponent [`decode`]'s compressed-point square root raises `α` to. +const SQRT_EXPONENT: [u64; 9] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000080, +]; + +/// Converts 66 big-endian bytes to little-endian `u64` limbs. +pub fn limbs_from_be_bytes(bytes: &[u8; 66]) -> [u64; 9] { + let mut limbs = [0u64; 9]; + for i in 0..8 { + limbs[i] = u64::from_be_bytes(bytes[(58 - i * 8)..(66 - i * 8)].try_into().unwrap()); + } + // `bytes[0] << 8` occupies bits 8-15 and `bytes[1]` occupies bits 0-7 -- disjoint, so `|` and + // `^` are equivalent here (an accepted mutant, not a bug: this repo's stated policy for + // OR/XOR-over-disjoint-bits cases). + limbs[8] = (u64::from(bytes[0]) << 8) | u64::from(bytes[1]); + limbs +} + +/// Converts little-endian `u64` limbs to 66 big-endian bytes. +pub fn be_bytes_from_limbs(limbs: &[u64; 9]) -> [u8; 66] { + let mut out = [0u8; 66]; + for i in 0..8 { + out[(58 - i * 8)..(66 - i * 8)].copy_from_slice(&limbs[i].to_be_bytes()); + } + let top = limbs[8] as u16; + out[0..2].copy_from_slice(&top.to_be_bytes()); + out +} + +fn limbs_less_than_p(limbs: &[u64; 9]) -> bool { + let (_, borrow) = nat::sub(limbs, &P_LIMBS); + borrow == 1 +} + +fn either_out_of_range(x_limbs: &[u64; 9], y_limbs: &[u64; 9]) -> bool { + !limbs_less_than_p(x_limbs) || !limbs_less_than_p(y_limbs) +} + +/// `y^2 == x^3 - 3x + b`. +fn is_on_curve(x: &P521FieldElement, y: &P521FieldElement) -> bool { + let lhs = y.mul(y); + let x_cubed = x.mul(x).mul(x); + let three_x = x.add(x).add(x); + let rhs = x_cubed.sub(&three_x).add(&P521FieldElement::from_limbs(B_LIMBS)); + lhs == rhs +} + +/// `base^SQRT_EXPONENT mod p`. Plain (not masked) square-and-multiply: only ever runs on public +/// data (an incoming encoded point), like [`crate::p256_sec1::pow_public`]. +/// +/// Flipping the `== 1` bit-test to `!= 1` here is an accepted mutant, not a bug: it computes +/// `alpha^(p - E)` instead of `alpha^E` (`E = SQRT_EXPONENT`), and since `alpha^(p-1) == 1` for any +/// `alpha != 0` (Fermat), `alpha^(p-E) == alpha / alpha^E == alpha / beta` where `beta = alpha^E`. +/// `[`decode`]`'s only use of the result is via `beta.mul(&beta) != alpha`, and `(alpha/beta)^2 == +/// alpha` iff `beta^2 == alpha` (multiply both sides by `alpha`) -- so the mutated exponent +/// reproduces the exact same accepted `beta` when `alpha` is a residue, and the exact same +/// rejection when it isn't. Verified (not checked in) against 2000 random field elements, split +/// roughly evenly between residues and non-residues. +fn pow_public(base: &P521FieldElement, exponent_limbs: &[u64; 9]) -> P521FieldElement { + let mut result = P521FieldElement::ONE; + for limb_idx in (0..9).rev() { + let limb = exponent_limbs[limb_idx]; + let bit_count = if limb_idx == 8 { 9 } else { 64 }; + for bit in (0..bit_count).rev() { + result = result.square(); + if (limb >> bit) & 1 == 1 { + result = result.mul(base); + } + } + } + result +} + +/// SEC 1 §2.3.3 case 3: `04 || X || Y`, 133 bytes. +pub fn encode_uncompressed(x: &P521FieldElement, y: &P521FieldElement) -> [u8; 133] { + let mut out = [0u8; 133]; + out[0] = 0x04; + out[1..67].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out[67..133].copy_from_slice(&be_bytes_from_limbs(&y.to_limbs())); + out +} + +/// SEC 1 §2.3.3 case 2: `02 || X` or `03 || X`, 67 bytes, tag selected by `y mod 2`. +pub fn encode_compressed(x: &P521FieldElement, y: &P521FieldElement) -> [u8; 67] { + let mut out = [0u8; 67]; + out[0] = if y.to_limbs()[0] & 1 == 0 { 0x02 } else { 0x03 }; + out[1..67].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out +} + +/// Decodes and validates a SEC 1 §2.3.4 point encoding: 67 bytes (compressed) or 133 bytes +/// (uncompressed). See [`crate::p256_sec1::decode`]'s docs for exactly which checks this performs. +pub fn decode(bytes: &[u8]) -> Option<(P521FieldElement, P521FieldElement)> { + match bytes.len() { + 67 => { + let y_tilde: u64 = match bytes[0] { + 0x02 => 0, + 0x03 => 1, + _ => return None, + }; + let x_bytes: [u8; 66] = bytes[1..67].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + if !limbs_less_than_p(&x_limbs) { + return None; + } + let x = P521FieldElement::from_limbs(x_limbs); + let x_cubed = x.mul(&x).mul(&x); + let three_x = x.add(&x).add(&x); + let alpha = x_cubed.sub(&three_x).add(&P521FieldElement::from_limbs(B_LIMBS)); + let beta = pow_public(&alpha, &SQRT_EXPONENT); + if beta.mul(&beta) != alpha { + return None; + } + let beta_parity = beta.to_limbs()[0] & 1; + let y = if beta_parity == y_tilde { beta } else { beta.negate() }; + Some((x, y)) + } + 133 => { + if bytes[0] != 0x04 { + return None; + } + let x_bytes: [u8; 66] = bytes[1..67].try_into().ok()?; + let y_bytes: [u8; 66] = bytes[67..133].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + let y_limbs = limbs_from_be_bytes(&y_bytes); + if either_out_of_range(&x_limbs, &y_limbs) { + return None; + } + let x = P521FieldElement::from_limbs(x_limbs); + let y = P521FieldElement::from_limbs(y_limbs); + if !is_on_curve(&x, &y) { + return None; + } + Some((x, y)) + } + _ => None, + } +} + +/// Decodes and validates an encoded public key (see [`decode`]) directly into a +/// [`P521JacobianPoint`]. +pub fn decode_point(bytes: &[u8]) -> Option { + let (x, y) = decode(bytes)?; + Some(P521JacobianPoint::from_affine(x, y)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn p_is_3_mod_4() { + assert_eq!(P_LIMBS[0] & 0b11, 3); + } + + #[test] + fn limbs_less_than_p_boundary_cases() { + assert!(limbs_less_than_p(&[0; 9])); + + let mut p_minus_1 = P_LIMBS; + p_minus_1[0] -= 1; + assert!(limbs_less_than_p(&p_minus_1)); + + assert!(!limbs_less_than_p(&P_LIMBS)); + + // every low limb of p is already saturated (0xffffffffffffffff); only the top limb (0x1ff) + // has room to bump without overflowing the u64 itself. + let mut p_with_bumped_limb = P_LIMBS; + p_with_bumped_limb[8] += 1; + assert!(!limbs_less_than_p(&p_with_bumped_limb)); + } + + #[test] + fn either_out_of_range_requires_only_one_coordinate_to_be_out_of_range() { + let in_range = [0u64; 9]; + let out_of_range = P_LIMBS; + + assert!(!either_out_of_range(&in_range, &in_range)); + assert!(either_out_of_range(&out_of_range, &in_range)); + assert!(either_out_of_range(&in_range, &out_of_range)); + assert!(either_out_of_range(&out_of_range, &out_of_range)); + } + + #[test] + fn be_bytes_round_trip() { + let limbs: [u64; 9] = [ + 0x1122334455667788, 0x99aabbccddeeff00, 0x0102030405060708, 0x1020304050607080, + 0x0a0b0c0d0e0f1011, 0x1213141516171819, 0x2021222324252627, 0x2829303132333435, + 0x0000000000000123, + ]; + assert_eq!(limbs_from_be_bytes(&be_bytes_from_limbs(&limbs)), limbs); + } +} diff --git a/crypto/ec/src/p521_wnaf.rs b/crypto/ec/src/p521_wnaf.rs new file mode 100644 index 000000000..681ae8e73 --- /dev/null +++ b/crypto/ec/src/p521_wnaf.rs @@ -0,0 +1,151 @@ +//! Interleaved windowed-NAF (wNAF) Shamir's-trick multiplication: `[u]G + [v]Q`, for P-521 ECDSA +//! verification. Identical algorithm to [`crate::p256_wnaf`] -- see that module's docs for the +//! full derivation and verification methodology -- with the width and limb count swapped for +//! P-521's (`WNAF_LEN = 522`, one more than `n`'s 521-bit length, same margin as P-256's `257 = +//! 256 + 1`). +//! +//! `G`'s odd multiples are a compile-time affine table (`p521_wnaf_table`, width 7) added by mixed +//! addition; only `Q`'s table is built per call. See [`crate::p256_wnaf`]. + +use crate::nat; +use crate::p521::P521FieldElement; +use crate::p521_point::P521JacobianPoint; +use crate::p521_scalar::P521PublicScalar; +use crate::p521_wnaf_table::{G_ODD_MULTIPLES_X, G_ODD_MULTIPLES_Y}; + +/// Window for `Q`, whose odd multiples are computed per call: 8 point additions to build the +/// table, one addition per ~6 digits to use it. +const WIDTH: usize = 5; +/// Window for `G`, whose odd multiples are a compile-time table +/// ([`G_ODD_MULTIPLES_X`]/[`G_ODD_MULTIPLES_Y`]): nothing to build, so the widest window a +/// signed `i8` digit allows, giving one addition per ~8 digits instead of ~6, and every one of +/// them a mixed addition against an affine entry. +const G_WIDTH: usize = 7; +const WNAF_LEN: usize = 522; +// Mutating this expression (e.g. `WIDTH - 2` -> `+ 2`) only grows the table `odd_multiples` builds +// and `lookup_signed` reads from; the actual digit range `compute_wnaf` can produce is bounded by +// the `WIDTH` passed to it (unchanged), so the extra entries are simply never indexed -- an +// accepted mutant. +const ODD_MULTIPLE_COUNT: usize = 1 << (WIDTH - 2); + +/// `[u]G + [v]Q`. +pub fn shamir_multiply( + u: &P521PublicScalar, + v: &P521PublicScalar, + q: &P521JacobianPoint, +) -> P521JacobianPoint { + let du = compute_wnaf(u.to_limbs(), G_WIDTH); + let dv = compute_wnaf(v.to_limbs(), WIDTH); + + let table_q = odd_multiples(q); + + let mut r = P521JacobianPoint::INFINITY; + for i in (0..WNAF_LEN).rev() { + r = r.double(); + if let Some((x, y)) = lookup_signed_g(du[i]) { + r = r.add_vartime_affine(&x, &y); + } + if let Some(add_q) = lookup_signed(&table_q, dv[i]) { + r = r.add_vartime(&add_q); + } + } + r +} + +fn shr1(limbs: &[u64; 9]) -> [u64; 9] { + [ + (limbs[0] >> 1) | ((limbs[1] & 1) << 63), + (limbs[1] >> 1) | ((limbs[2] & 1) << 63), + (limbs[2] >> 1) | ((limbs[3] & 1) << 63), + (limbs[3] >> 1) | ((limbs[4] & 1) << 63), + (limbs[4] >> 1) | ((limbs[5] & 1) << 63), + (limbs[5] >> 1) | ((limbs[6] & 1) << 63), + (limbs[6] >> 1) | ((limbs[7] & 1) << 63), + (limbs[7] >> 1) | ((limbs[8] & 1) << 63), + limbs[8] >> 1, + ] +} + +/// The width-`width` wNAF digits of `k` (`width` at most 7, so a digit fits an `i8`), LSB-first; unused trailing entries are `0`. Not +/// constant time: only ever called on public scalars (see the module docs). +fn compute_wnaf(k_limbs: [u64; 9], width: usize) -> [i8; WNAF_LEN] { + let mut digits = [0i8; WNAF_LEN]; + let mut k = k_limbs; + let mut pos = 0; + while k != [0, 0, 0, 0, 0, 0, 0, 0, 0] { + if k[0] & 1 == 1 { + // Mutating this mask (e.g. `(1 << width) - 1` -> `+ 1`, giving 33 instead of 31) is an + // accepted mutant, not a bug: whatever mask is used, `digit` always keeps `k[0]`'s low + // (odd) bit, so it's always odd -- the only invariant `lookup_signed` and the + // subtract-or-add-then-`shr1` step below actually rely on. A degenerate mask just makes + // this compute a non-minimal (but still exactly reconstructing `k`) signed-digit + // sequence, e.g. the 33-mask case collapses every digit to `+-1`, a valid if wasteful + // plain binary representation -- verified (not checked in) that both this crate's + // cross-checked-against-the-comb-multiplier property test and a hand run of this exact + // mutation still produce the correct `[k]G` output. + let mut digit = (k[0] & ((1 << width) - 1)) as i16; + if digit >= 1 << (width - 1) { + digit -= 1 << width; + } + k = if digit >= 0 { + let (new_k, borrow) = nat::sub(&k, &[digit as u64, 0, 0, 0, 0, 0, 0, 0, 0]); + debug_assert_eq!(borrow, 0); + new_k + } else { + let (new_k, carry) = nat::add(&k, &[(-digit) as u64, 0, 0, 0, 0, 0, 0, 0, 0]); + debug_assert_eq!(carry, 0); + new_k + }; + debug_assert!(pos < WNAF_LEN, "wNAF encoding overflowed its verified bound"); + digits[pos] = digit as i8; + } + k = shr1(&k); + pos += 1; + } + digits +} + +/// The odd multiples `1*p, 3*p, 5*p, ..., (2*ODD_MULTIPLE_COUNT - 1)*p`, indexed so entry `i` +/// holds `(2i+1)*p`. +fn odd_multiples(p: &P521JacobianPoint) -> [P521JacobianPoint; ODD_MULTIPLE_COUNT] { + let double_p = p.double(); + let mut table = [*p; ODD_MULTIPLE_COUNT]; + let mut cur = *p; + for entry in table.iter_mut() { + *entry = cur; + cur = cur.add_vartime(&double_p); + } + table +} + +/// `digit * p` where `p` is the point `table` was built from ([`odd_multiples`]), or `None` for +/// digit `0`. +fn lookup_signed( + table: &[P521JacobianPoint; ODD_MULTIPLE_COUNT], + digit: i8, +) -> Option { + // Every accepted-mutant survivor below relies on `digit` always being odd (it's built from an + // odd `k[0]` in `compute_wnaf`, see that function's docs): `digit > 0` vs `>= 0` agree once the + // `digit == 0` case is already handled above, and for odd `digit`, `(digit - 1) / 2 == digit / + // 2` (and symmetrically for `-digit`), since integer division already floors an odd numerator + // down to the same value subtracting 1 first would. + if digit == 0 { + None + } else if digit > 0 { + Some(table[(digit as usize - 1) / 2]) + } else { + Some(table[(-digit as usize - 1) / 2].negate()) + } +} + +/// `digit * G` for a width-[`G_WIDTH`] digit, read from the compile-time table of odd multiples +/// of `G` as affine coordinates (negating `y` for a negative digit), or `None` for digit `0`. +fn lookup_signed_g(digit: i8) -> Option<(P521FieldElement, P521FieldElement)> { + if digit == 0 { + return None; + } + let index = (digit.unsigned_abs() as usize - 1) / 2; + let x = P521FieldElement::from_limbs(G_ODD_MULTIPLES_X[index]); + let y = P521FieldElement::from_limbs(G_ODD_MULTIPLES_Y[index]); + Some(if digit > 0 { (x, y) } else { (x, y.negate()) }) +} diff --git a/crypto/ec/src/p521_wnaf_table.rs b/crypto/ec/src/p521_wnaf_table.rs new file mode 100644 index 000000000..07ec1d947 --- /dev/null +++ b/crypto/ec/src/p521_wnaf_table.rs @@ -0,0 +1,361 @@ +//! The checked-in odd multiples of the base point `G` for [`crate::p521_wnaf`]'s fixed side: +//! `G_ODD_MULTIPLES_X[i]`/`G_ODD_MULTIPLES_Y[i]` are the affine coordinates of `(2i + 1) G`, for +//! `i` in `0..32`, as little-endian `u64` limbs -- the table a width-7 wNAF over `G` indexes. +//! `Q`'s table is built at run time because `Q` is the signer's key; `G` is the curve's constant, +//! so its table need not be. +//! +//! Generated in Python from the affine group law (SP 800-186 Appendix A.1.1) and SP 800-186 §3.2.1.5's `G`, +//! independently of this crate's arithmetic, and checked there by `n * G == infinity`; this +//! module's own test regenerates every entry with the crate's point arithmetic and compares. + +/// `x` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_X: [[u64; 9]; 32] = [ + [ + 0xf97e7e31c2e5bd66, 0x3348b3c1856a429b, 0xfe1dc127a2ffa8de, 0xa14b5e77efe75928, + 0xf828af606b4d3dba, 0x9c648139053fb521, 0x9e3ecb662395b442, 0x858e06b70404e9cd, + 0x00000000000000c6, + ], + [ + 0xa5919d2ede37ad7d, 0xaeb490862c32ea05, 0x1da6bd16b59fe21b, 0xad3f164a3a483205, + 0xe5ad7a112d7a8dd1, 0xb52a6e5b123d9ab9, 0xd91d6a64b5959479, 0x3d352443de29195d, + 0x00000000000001a7, + ], + [ + 0xd5ab5096ec8f3078, 0x29d7e1e6d8931738, 0x7112feaf137e79a3, 0x383c0c6d5e301423, + 0xcf03dab8f177ace4, 0x7a596efdb53f0d24, 0x3dbc3391c04eb0bf, 0x2bf3c52927a432c7, + 0x0000000000000065, + ], + [ + 0x01cead882816ecd4, 0x6f953f50fdc2619a, 0xc9a6df30dce3bbc4, 0x8c308d0abfc698d8, + 0xf018d2c2f7114c5d, 0x5f22e0e8f5483228, 0xeeb65fda0b073a0c, 0xd5d1d99d5b7f6346, + 0x0000000000000056, + ], + [ + 0x1f45627967cbe207, 0x4f50babd85cd2866, 0xf3c556df725a318f, 0x7429e1396134da35, + 0x2c4ab145b8c6b665, 0xed34541b98874699, 0xa2f5bf157156d488, 0x5389e359e1e21826, + 0x0000000000000158, + ], + [ + 0xecc0e02dda0cdb9a, 0x015c024fa4c9a902, 0xd19b1aebe3191085, 0xf3dbc5332663da1b, + 0x43ef2c54f2991652, 0xed5dc7ed7c178495, 0x6f1a39573b4315cf, 0x75841259fdedff54, + 0x000000000000008a, + ], + [ + 0x1887848d32fbcda7, 0x4bec3b00ab38eff8, 0x3550a5e79ab88ee9, 0x32c45908e03c996a, + 0x4eedd2beaf5b8661, 0x93f736cde1b4c238, 0xd7865d2b4924861a, 0x3e98f984c396ad9c, + 0x000000000000007e, + ], + [ + 0xe9afe337bcb8db55, 0x9b8d96981e3f92bd, 0x7875bd1c8fc0331d, 0xb91cce27dbd00ffe, + 0xd697b532df128e11, 0xb8fbcc30b40a0852, 0x41558fc546d4300f, 0x6ad89abcb92465f0, + 0x000000000000006b, + ], + [ + 0x76f817a853110ae0, 0xf8c3042af0d1a410, 0xdf4e799b5681380f, 0x760a69e674fe0287, + 0xd66524f269250858, 0x99ee9e269fa2b3b4, 0xa0b874645923906a, 0x0ddb707f130eda13, + 0x00000000000001b0, + ], + [ + 0x78ff0b2418d6a19b, 0xfecf431e725bbde4, 0x9232557d7a45970d, 0xfa3b309636266967, + 0xfff0acdb3790e7f1, 0x45b77e0755df547e, 0xc0f948c2d5a1a072, 0x8dcce486419c3487, + 0x0000000000000099, + ], + [ + 0x42ef399693c8c9ed, 0x37ac920393a46d2d, 0xd9497eaed827d75b, 0x46257eae4d62a309, + 0x19523e759c467fa9, 0x268bb98c2ed15e98, 0x3cc8550859ed3b10, 0xcbb2c11a742bd2fb, + 0x00000000000001a1, + ], + [ + 0xa1c6a5ece2af535c, 0x07e1f1ac26ae5806, 0xe9e3f8e9a5ec53e2, 0x1f6f83fc9af5b0af, + 0x490d234d0cdd4510, 0xee7a39ba785fb282, 0xdb1cacec5f547b8e, 0x0b275d7290159376, + 0x00000000000001ae, + ], + [ + 0x3f8c2460bf70ace0, 0xe41e0eb1c25d3fb1, 0x56e87e2aa648ff27, 0xa91856917c36ee4d, + 0x1c772c8c5499994a, 0x0073102651b107b1, 0xa35874a6f5dff9d2, 0xe84c6d5c5a9a1834, + 0x0000000000000154, + ], + [ + 0x357643017002d68b, 0xb215604492ec4568, 0x51353aeda0d3163c, 0x80b2123da719d483, + 0x33f35187e135854d, 0x8739535d0e4f862c, 0x62a4d4eb889e646f, 0x373edf8218f9b6a7, + 0x0000000000000160, + ], + [ + 0x2c3927618eda25dc, 0xf33d8595d51f6d96, 0x4003ab8e847ffb9e, 0x5ca9bc06876d7291, + 0x28bef38f7664a130, 0xf9f45131e86265ec, 0xb3c1fbfcb65a085f, 0xc644d6c94b68287d, + 0x0000000000000174, + ], + [ + 0x2257d0e0c16a8803, 0x0dcfb5e488e24812, 0xfc14c1ac09cd6b22, 0xd65543a904c9d429, + 0x7260a83ca5e7726c, 0xb574589657c2a661, 0x006a58cdb7307b7d, 0xe9920cf30f0c6615, + 0x00000000000000d8, + ], + [ + 0x5ea2e1fc649f308d, 0xa5ec59186b2ed12d, 0xe9a519a57aa53ac2, 0xabdbea7e2b77ef1a, + 0xf381421a74236df0, 0x52086d482be92613, 0x8c76eb4e3c76f58e, 0x4195f0978fb969e6, + 0x0000000000000028, + ], + [ + 0xdc1039c9ccd7d718, 0xa92ff614b5dcc8d8, 0xee4a618608f6b2ce, 0xb4806c84fda74023, + 0xc4a780b56f97392b, 0xa267a642b593e0bf, 0x398b62069e0ba392, 0xc300757549630a78, + 0x00000000000000dd, + ], + [ + 0xf1ac4d59b557a36f, 0x8c64e76ae4780273, 0x0e58e26ee0980df2, 0xbd445763a2885604, + 0x29ed0ae6af9d5749, 0xc35c5d56c1d59e42, 0x39e798352ded5867, 0x4f64a6c21832b671, + 0x0000000000000094, + ], + [ + 0x0b704119ee33b77c, 0x5b4fa1d48083af67, 0xac0bf434e5fa3179, 0x256b0cc58626a1e2, + 0x38d9fd62359c6ea7, 0x9e9661a49b5b9072, 0x5264126356a49902, 0xa0b8f411fbad6075, + 0x0000000000000124, + ], + [ + 0x8e00ce952624381e, 0xddfda1a522cc2af6, 0x4c08c3ddc8297bfa, 0x16b931eaf495ccce, + 0x85b85f23864a60c8, 0x52a523e4b28998a6, 0x63ebfd9d28830825, 0xa140ed79e85e24a7, + 0x0000000000000175, + ], + [ + 0x713e3083224f497e, 0x51f0b62fdee07d20, 0xa3a74e7a9b3d85ca, 0xfc66ad7aed7d37dd, + 0x8ef0f94438fea396, 0x70678aa2ec1419af, 0xd55022d90544d8a0, 0x0148a165ec58e4fe, + 0x000000000000018c, + ], + [ + 0xa32dec600fc95c1a, 0xc2954607b66b70c4, 0xdc8ea1ace5703f0f, 0x79189b7df8a1f2d9, + 0x7b2df5e3f1972867, 0x7a58f7f28c147cc0, 0x79596b8fff2bc020, 0x69d186aca09e53b8, + 0x0000000000000049, + ], + [ + 0x2fda63c9abd59d11, 0x0a56a130d1ecbb67, 0x9cee75a3a9c4dd66, 0x381864a0b9e922ac, + 0x97233106c71dd0b2, 0xe4e4fe2714033ff8, 0x9cf5083ecc35882c, 0xe31f8907048afd5f, + 0x00000000000000af, + ], + [ + 0x0c8cb45049efc0ad, 0x7258dab1ac4c04ca, 0x91d8c84e2e345fa8, 0x6bc2a2df52f62842, + 0xf581b8b111dea9eb, 0x9d45c347ae499839, 0xcba40a63bbaba0d3, 0xcb98fa3c0b8c1d57, + 0x00000000000000a5, + ], + [ + 0xa8d916fffbcc9504, 0xfba689ef1d7be2dd, 0x1321ae1b1054cbc0, 0xc2edfe6af07390df, + 0x3ba316cbf9beea26, 0xfdf9f5bde34fe9ba, 0x025d93b68145f3cc, 0x395ba51e278415a2, + 0x0000000000000168, + ], + [ + 0x6285684cccb69906, 0x08153da9c9880816, 0xd3af581fba4fe12b, 0x4e210e637209a78e, + 0x27e82f6f948100c4, 0xf8688be916895fb7, 0x7b3d0ffd310306df, 0xf6e249195ee693f7, + 0x0000000000000143, + ], + [ + 0x84ad7ccebd470f5e, 0x7fe9ab4bda96ac4e, 0xbe17486b1e0b549a, 0x8cca93917cad27e6, + 0x6472d4028e8da1a1, 0x7cb03e9fd9d0a79d, 0xb551e4155c6daca9, 0x52fc4b6d310ce7c4, + 0x000000000000004b, + ], + [ + 0x3475330a4e9a13e2, 0x8372c8fff95c8450, 0xf66fd5bc64b8a520, 0x4af300bbde9118b2, + 0xef3c6d77a521b9f8, 0x5a8defe72e6dbf85, 0x4463da75efb25ffa, 0x53c3e7fcf3c902e0, + 0x00000000000000dc, + ], + [ + 0x63be8a26eb16686b, 0xcba8524ec3472088, 0x1a90342d64373a8a, 0x055693c9b1344c69, + 0xaae5c2934c222920, 0xc7223c5d98901999, 0x063c60342df29ead, 0xaeb454ad31876773, + 0x0000000000000035, + ], + [ + 0xcb53d8cfcfc376a1, 0x952bad1671543c41, 0xa5e7fce59a7b32d7, 0x7193c11a77dc0b39, + 0x4cd724b1be8b1717, 0x8b6cd17d5b660d1a, 0x92ca7c732a1c7d8f, 0x544c4a011407425c, + 0x0000000000000115, + ], + [ + 0x0f5beb6fce8888e5, 0x75af7d025770ac8d, 0xc63996847586265e, 0x3ae99661308e125e, + 0xe7f54fe4d51b0845, 0x1ca21b5affa0ddc1, 0xe82f799acb0a6e8e, 0x32753b64640c457f, + 0x00000000000001c1, + ], +]; + +/// `y` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_Y: [[u64; 9]; 32] = [ + [ + 0x88be94769fd16650, 0x353c7086a272c240, 0xc550b9013fad0761, 0x97ee72995ef42640, + 0x17afbd17273e662c, 0x98f54449579b4468, 0x5c8a5fb42c7d1bd9, 0x39296a789a3bc004, + 0x0000000000000118, + ], + [ + 0x5f588ca1ee86c0e5, 0xf105c9bc93a59042, 0x2d5aced1dec3c70c, 0x2e2dd4cf8dc575b0, + 0xd2f8ab1fa355ceec, 0xf1557fa82a9d0317, 0x979f86c6cab814f2, 0x9b03b97dfa62ddd9, + 0x000000000000013e, + ], + [ + 0x173cc3e8deb090cb, 0xd1f007257354f7f8, 0x311540211cf5ff79, 0xbb6897c9072cf374, + 0xedd817c9a0347087, 0x1cd8fe8e872e0051, 0x8a2b73114a811291, 0xe6ef1bdd6601d6ec, + 0x000000000000015b, + ], + [ + 0x5c6b8bc90525251b, 0x9e76712a5ddefc7b, 0x9523a34591ce1a5f, 0x6bd0f293cdec9e2b, + 0x71dbd98a26cbde55, 0xb5c582d02824f0dd, 0xd1d8317a39d68478, 0x2d1b7d9baaa2a110, + 0x000000000000003d, + ], + [ + 0x3aa0ea86b9ad2a4e, 0x736c2ae928880f34, 0x0ff56ecf4abfd87d, 0x0d69e5756057ac84, + 0xc825ba263ddb446e, 0x3088a654ee1cebb6, 0x0b55557a27ae938e, 0x2e618c9a8aedf39f, + 0x000000000000002a, + ], + [ + 0x58874f92ce48c808, 0xdcac80e3f4819b5d, 0x3892331914a95336, 0x1bc8a90e8b42a4ab, + 0xed2e95d4e0b9b82b, 0x3add566210bd0493, 0x9d0ca877054fb229, 0xfb303fcbba212984, + 0x0000000000000096, + ], + [ + 0x291a01fb022a71c9, 0x6199eaaf9117e9f7, 0x26dfdd351cbfbbc3, 0xc1bd5d5838bc763f, + 0x9c7a67ae5c1e212a, 0xced50a386d5421c6, 0x1a1926daa3ed5a08, 0xee58eb6d781feda9, + 0x0000000000000108, + ], + [ + 0x56343480a1475465, 0x46fd90cc446abdd9, 0x2148e2232c96c992, 0x7e9062c899470a80, + 0x4b62106997485ed5, 0xdf0496a9bad20cba, 0x7ce64d2333edbf63, 0x68da271571391d6a, + 0x00000000000001b4, + ], + [ + 0x35b9cb7c70e64647, 0xe6905594c2b755f5, 0xd2f6757f16adf420, 0xf9da564ef6dd0bf0, + 0x8d68ac2b22a1323d, 0xb799534cf69910a9, 0xc111d4e4aeddd106, 0x683f1d7db16576db, + 0x0000000000000085, + ], + [ + 0xa9091a695bfd0575, 0xf5a4d89ea9fbfe44, 0xb0ec39991631c377, 0x73ad963ff2eb8cf9, + 0xcc50eee365457727, 0x67d28aee2b7bcf4a, 0xc3942497535b245d, 0xd5da0626a021ed5c, + 0x0000000000000137, + ], + [ + 0xe60bc43c9cba4df5, 0x7c9b0f17649ccb61, 0xbeb43a372c63eec5, 0xdf741a53da483295, + 0x180a296f6bafa7f7, 0xe83c0059c5193e6c, 0x2c12da7c5e40ce62, 0x209d7d4f8eeb3d48, + 0x000000000000011a, + ], + [ + 0xd815c3536fa0d000, 0x213b4450a8d23856, 0x3c27b27bb07dd0c2, 0x10843361ee97fcf4, + 0xb431647844c2dc0e, 0x7d759ff890d05832, 0x68a2858fc068471c, 0xc97a825e53853806, + 0x00000000000000f2, + ], + [ + 0x4325bce404c78230, 0xeede2a54672e6b6d, 0xd1e2370a6a5972f5, 0xdee3543572fbc1a0, + 0xf66c2888151666a6, 0x15a923eb0022a0c7, 0xe22a28f80bb60d3f, 0x0fdce9171910473a, + 0x00000000000000cd, + ], + [ + 0xf3e6aeca5d90b740, 0x463ffe709d45acb3, 0x13b874f4a8bb572e, 0x1efa491ed92ebc54, + 0x4a56f78e1a1b2201, 0x9fd193c5cf52c3bb, 0xe5828401ac06a3fa, 0x597050014dcfe1c5, + 0x00000000000000f1, + ], + [ + 0x187bbbc4821a0c30, 0x0679927c26ebbfbd, 0x50c2732d706d303f, 0xbe0e21952ce0d90b, + 0xb5cf5eb795ad34b7, 0x0233ef8fcb6441fc, 0x05acc95b41b7b782, 0xf3a7c2f87f419e68, + 0x000000000000011a, + ], + [ + 0xca4677c739792d19, 0xaa1bd97c7b54318a, 0x139a868cae4cc263, 0xf76b8c3244d14790, + 0x0aefb72cbed1aa30, 0x8b5406328f10c806, 0xdf09c13a214a30ec, 0xb023b5454a663987, + 0x0000000000000127, + ], + [ + 0x6be95a3dd3e11c4d, 0x88effd5c228b58f3, 0x00bd7216c16deb3a, 0xe7656ecbf3d138bf, + 0x9e016769614ac5f2, 0x24d513abe063c663, 0x7b7a3bc869056d3a, 0x43eb08c656dc636b, + 0x000000000000007e, + ], + [ + 0x3eb1d3ef241e07f4, 0xdeba4db422640a4d, 0x5c212522ee69e797, 0x9ab1178bd2c70142, + 0x462796591a31db50, 0xa510936cb5d85bcf, 0x1b83431f0c30dbbf, 0xbd2d07f6ac7fc5cc, + 0x00000000000000c1, + ], + [ + 0x312bf98394fb2a03, 0x31a4be4056988296, 0xb85c564995a057c3, 0xdcbdbc2471c83f81, + 0x26317da6b7991305, 0xfe4e6ff21865f859, 0x0c4b9624f26cb192, 0xb7ee1b02028ab741, + 0x0000000000000150, + ], + [ + 0xd8dcdc61228b61a6, 0xf212e74b698e40ab, 0xa3caf2415944e762, 0x18dc59feb96825aa, + 0xdc0b1240c690db48, 0x68937baa8796154c, 0x602a9a406bbd399c, 0x29616edc7335dce3, + 0x0000000000000010, + ], + [ + 0xebeb760ec1028ecf, 0xf75dd758e7f3a3ec, 0x052a6e551fa28ebd, 0xb39e0e11ecf327da, + 0x23de821b22c82111, 0xab59e580e9ee5632, 0x36f21343ca399be7, 0x9696d71855e2d4ed, + 0x00000000000001b0, + ], + [ + 0x895829067683adbf, 0x94edb92f76b688d2, 0x932d602b547ce17a, 0x67fd6098879b1cf1, + 0x7a3037819f1a0bec, 0x2677e91db1d144cd, 0xebf7a83c7aa3bf24, 0xbbaa1f099e78869a, + 0x000000000000010a, + ], + [ + 0x9498a7cd000903a9, 0x3463f5d90b37564f, 0xe6ed9e879be328e1, 0xb8d5b2802eadae85, + 0x35584a1bbcdb40a9, 0x15862a4232708841, 0x4d6ac21f3149fc7b, 0x1ec9b50cd8d3847d, + 0x00000000000001e8, + ], + [ + 0xd3065d2f1d90e1d6, 0x11ca41599f10673b, 0xebb760f478d825e7, 0x9276080b904982a6, + 0xecea7f3276f06497, 0x605f079349209f88, 0xb4290cbbb54ff69a, 0xf95dc8657275fe9d, + 0x0000000000000012, + ], + [ + 0x0b9bf46a2c8884b4, 0x4b963fbfb0b88b1d, 0x0c10f2ceebb72bb4, 0x5dd9775543d575c2, + 0xb072c39d9da8bf83, 0x749ee467877e5b7e, 0x1409b01bf72e151b, 0xdc95654090d77b97, + 0x0000000000000015, + ], + [ + 0x8eb9c45edd5c087b, 0x0600aabed3a89e18, 0x849c36096e3aeba5, 0x97bc2b68badb600c, + 0x24e5b174adeb9b3c, 0x52dd878f21c480ce, 0x39d9531942d3f579, 0xcc10f3ca041a2456, + 0x0000000000000071, + ], + [ + 0xfd6ddaddd40c7861, 0x040a3dfbc4abee6a, 0x0f6a7a9de3b4cf8c, 0x4fdf64f503cf3bb3, + 0x35437e8053d10cb1, 0x7dc73fdde42c2169, 0xc5611a0257510987, 0x3e8fcc9618eb2a74, + 0x0000000000000105, + ], + [ + 0x7a196cd230a36ef2, 0xfa03a23006a096ea, 0xd69609e345b53586, 0x10aa85895c5a084c, + 0x00fb114a7dbae155, 0x619f44311a16a0b6, 0x385ea7907a1a7b2e, 0x85e54fe81461ae21, + 0x0000000000000035, + ], + [ + 0x33ad7f7fd9c4248f, 0xa9493190c62a5532, 0x4a3f82056a929f73, 0x3482530d0d3bd86a, + 0xb62e7eb390dff3ea, 0xb13d0dd2caf9d989, 0x68073ee6c4d4f8f7, 0xe88fccd4fdbd992b, + 0x0000000000000074, + ], + [ + 0xcf4aa03c5381fa2e, 0xfd82c38cfaffe51a, 0xc9fd5fddd64ffec3, 0xb8cf8c44bc83d0b8, + 0xe22f2ef3cb6efd45, 0x15a48db3660903f4, 0xdb0b0ca0aff1ba7f, 0x7ecbec147e7e43b4, + 0x0000000000000018, + ], + [ + 0xb8eff8cd4a17604b, 0x56f22ee9c3fe5e24, 0xa834ff603afa032b, 0x03f78d54b7f553a8, + 0xec097a3aed58c6a0, 0x79af485fef422cbf, 0x07996d2a399c872c, 0x3df9c6c0ac6485b3, + 0x0000000000000115, + ], + [ + 0xe9d15ad2a03dba15, 0xaf657b5bf664a2ac, 0x13f78f82f0071283, 0x05d3dbdcec1896bc, + 0x595e8c353aa22380, 0x1a9e093286414006, 0x6d81ac89b205d796, 0x47e184197a053817, + 0x0000000000000153, + ], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::p521::P521FieldElement; + use crate::p521_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::p521_point::P521JacobianPoint; + + #[test] + fn table_matches_the_odd_multiples_of_g_computed_by_the_point_arithmetic() { + let g = P521JacobianPoint::from_affine( + P521FieldElement::from_limbs(G_X_LIMBS), + P521FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + let mut cur = g; + for i in 0..32 { + let (x, y) = cur.to_affine().expect("an odd multiple of G below n is never infinity"); + assert_eq!(x.to_limbs(), G_ODD_MULTIPLES_X[i], "entry {i}: x of {}G", 2 * i + 1); + assert_eq!(y.to_limbs(), G_ODD_MULTIPLES_Y[i], "entry {i}: y of {}G", 2 * i + 1); + cur = cur.add_vartime(&two_g); + } + } +} diff --git a/crypto/ec/src/sm2.rs b/crypto/ec/src/sm2.rs new file mode 100644 index 000000000..aadf8ee3a --- /dev/null +++ b/crypto/ec/src/sm2.rs @@ -0,0 +1,441 @@ +//! The SM2 base field, GF(p) for `p = 2^256 - 2^224 - 2^96 + 2^64 - 1`. +//! +//! Domain parameters are from the recommended curve in `draft-shen-sm2-ecdsa-02` Appendix D (the +//! IETF draft describing SM2, freshly downloaded from the IETF archive; the underlying Chinese +//! national standard is GB/T 32918.5-2017), whose hex value for `p` is `FFFFFFFE FFFFFFFF +//! FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFF` (256 bits, most significant word +//! first). [`P_LIMBS`] below is that same value in little-endian `u64` limbs; `a = p - 3` (the +//! draft's `a = FFFFFFFE FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF 00000000 FFFFFFFF FFFFFFFC`), the +//! same shortcut every NIST curve in this crate uses, so [`crate::sm2_point`] reuses +//! [`crate::p256_point`]'s `a = -3` doubling formula unchanged. +//! +//! # Reduction algorithm +//! +//! `p` is a generalized Mersenne number, so -- exactly as for [`crate::p256`] -- a 512-bit product +//! reduces to a fixed sum and difference of 256-bit terms assembled from the product's own 32-bit +//! words, with no multiplication at all. [`reduce`] uses the same bias-fold-subtract finish as +//! [`crate::p256::reduce`], but evaluates its term table one 32-bit column at a time rather than +//! one 256-bit row at a time -- see its own docs for why that pays here and not for P-256. +//! +//! **That table is derived here, not quoted.** SP 800-186 Appendix G.1 publishes such a table for +//! each NIST prime, but SM2 is not a NIST curve and neither `draft-shen-sm2-ecdsa-02` nor GB/T +//! 32918 gives one, so it was computed from `p` directly rather than taken from any document or +//! from another implementation. The derivation is three lines and reproducible: writing the +//! product as `A = (A15 || A14 || ... || A0)` in 32-bit words, +//! +//! ```text +//! A mod p == A_low + sum_{j=0..7} A_{8+j} * M_j (mod p), M_j = 2^(256 + 32j) mod p +//! ``` +//! +//! and each `M_j`, written in balanced base `2^32`, turns out to have only the small signed +//! coefficients below (`w0` least significant) -- which is exactly what makes the fold +//! multiplication-free: +//! +//! ```text +//! M_0: +1 0 -1 +1 0 0 0 +1 M_4: +1 +1 0 +1 +1 0 0 +2 +//! M_1: +1 +1 -1 0 +1 0 0 +1 M_5: +2 +1 -1 +2 +1 +1 0 +2 +//! M_2: +1 +1 0 0 0 +1 0 +1 M_6: +2 +2 -1 +1 +2 +1 +1 +2 +//! M_3: +1 +1 0 +1 0 0 +1 +1 M_7: +2 +2 0 +1 +1 +2 +1 +3 +//! ``` +//! +//! Collecting those coefficients column by column gives [`reduce`]'s `S`/`D` terms: a coefficient +//! of `+2` simply means that word appears in two `S` rows. The resulting expression was checked +//! against Python's arbitrary-precision `%` over 300,000 random `a*b` products with `a, b < p`, +//! plus `(p-1)*(p-1)`, `(p-1)*(p-2)` and the small cases, before being written here; the +//! extreme-operand known answers in `sm2_field_tests.rs` pin the same corners from outside. +//! +//! The identity `2^256 = p + C` with `C = 2^224 + 2^96 - 2^64 + 1` (i.e. `2^256 mod p = C`) +//! survives in two places: [`Sm2FieldElement::add`] uses it to correct a carry out of the top +//! limb, and [`reduce`]'s final fold uses it on the accumulator's top limb. + +use crate::nat; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// `p`, little-endian `u64` limbs (`draft-shen-sm2-ecdsa-02` Appendix D). +pub const P_LIMBS: [u64; 4] = + [0xffffffffffffffff, 0xffffffff00000000, 0xffffffffffffffff, 0xfffffffeffffffff]; + +/// `2^256 mod p = 2^224 + 2^96 - 2^64 + 1`, little-endian `u64` limbs. This is the constant +/// [`reduce`]'s fold uses in place of `2^256`. +const C_LIMBS: [u64; 4] = + [0x0000000000000001, 0x00000000ffffffff, 0x0000000000000000, 0x0000000100000000]; + +/// `p - 2`, little-endian `u64` limbs -- the public exponent [`Sm2FieldElement::invert`] raises +/// its base to, per Fermat's little theorem. +const P_MINUS_2_LIMBS: [u64; 4] = + [0xfffffffffffffffd, 0xffffffff00000000, 0xffffffffffffffff, 0xfffffffeffffffff]; + +/// `5p`, little-endian `u64` limbs -- the bias [`reduce`] starts its accumulator at so that +/// subtracting its four `D` terms can never take it below zero. See [`reduce`] for why `5` is the +/// right multiple. +const FIVE_P_LIMBS: [u64; 5] = [ + 0xfffffffffffffffb, 0xfffffffb00000004, 0xffffffffffffffff, 0xfffffffaffffffff, + 0x0000000000000004, +]; + +/// An element of the SM2 base field GF(p), always held in canonical reduced form (`< p`). +#[derive(Clone, Copy, Debug)] +pub struct Sm2FieldElement([u64; 4]); + +impl Sm2FieldElement { + /// The additive identity. + pub const ZERO: Self = Self([0, 0, 0, 0]); + + /// The multiplicative identity. + pub const ONE: Self = Self([1, 0, 0, 0]); + + /// Builds a field element from little-endian `u64` limbs, reducing once if `limbs >= p`. Any + /// `u64` limb pattern is accepted: since `limbs < 2^256 < 2*p`, a single conditional + /// subtraction of `p` always suffices to bring it into `[0, p)`. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + let (diff, borrow) = nat::sub(&limbs, &P_LIMBS); + let mut result = [0u64; 4]; + // borrow == 1 means limbs < p already; otherwise take the (single) reduced difference. + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut result); + Self(result) + } + + /// Returns the canonical little-endian `u64` limbs, `< p`. + pub fn to_limbs(&self) -> [u64; 4] { + self.0 + } + + /// The limbs exactly as this type stores them: the canonical value itself, so this is + /// [`Self::to_limbs`] by another name. Paired with [`Self::from_internal_limbs`] so the + /// crate's point arithmetic can select between the coordinates of two existing points without + /// re-reducing each one. + pub(crate) fn internal_limbs(&self) -> [u64; 4] { + self.0 + } + + /// Wraps limbs already in this type's stored representation (see [`Self::internal_limbs`]), + /// skipping the reduction and conversion [`Self::from_limbs`] performs. Only for values that + /// came out of another element of this type, or a constant computed in that representation: + /// the masked selects in the point arithmetic, and the comb table's infinity sentinel. Not + /// exposed, since an out-of-range value would break every equality and reduction assumption + /// downstream; the range is checked in debug builds. + pub(crate) fn from_internal_limbs(limbs: [u64; 4]) -> Self { + debug_assert_eq!( + crate::nat::sub(&limbs, &P_LIMBS).1, + 1, + "from_internal_limbs given a value >= p" + ); + Self(limbs) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + nat::is_zero(&self.0) + } + + /// `self + other mod p`. + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = nat::add(&self.0, &other.0); + // true_sum = sum + carry*2^256, and self,other < p so true_sum < 2p. + let (sum_plus_c, _) = nat::add(&sum, &C_LIMBS); // used when carry == 1: true_sum - p == sum + C + let (diff, borrow) = nat::sub(&sum, &P_LIMBS); // used when carry == 0: is sum >= p? + let mut when_no_carry = [0u64; 4]; + // borrow == 1 means sum < p already; otherwise take sum - p. + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_c, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod p`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = nat::sub(&self.0, &other.0); + let (corrected, _) = nat::add(&diff, &P_LIMBS); // used when borrow == 1: (self - other) + p + let mut result = [0u64; 4]; + // borrow == 1 means self < other, so the wrapped diff needs +p added back. + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod p`. + pub fn negate(&self) -> Self { + let (diff, _) = nat::sub(&P_LIMBS, &self.0); // p - self; never borrows since self < p + let mut result = [0u64; 4]; + // p - 0 == p, which is not canonical: 0 must negate to 0, not p. + ct::conditional_select(nat::is_zero(&self.0), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod p`. + pub fn mul(&self, other: &Self) -> Self { + Self(reduce(&widening_mul(&self.0, &other.0))) + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`widening_square`]. + /// + /// Used by the exponentiation loops ([`Self::invert`] and friends), which call it hundreds of + /// times in a row with nothing else competing for registers. Deliberately *not* used by this + /// curve's point arithmetic, which squares by calling `mul` with equal arguments: that was + /// tried and measured on P-256, and despite `square` being ~9% cheaper than `mul` in + /// isolation it made the constant-time comb multiplier consistently slower, because inlining + /// a third wide-multiply routine into that loop costs more than the saved partial products + /// return. See [`crate::p256::P256FieldElement::square`] for the numbers. + pub fn square(&self) -> Self { + Self(reduce(&widening_square(&self.0))) + } + + /// `self^-1 mod p`, or `0` if `self` is `0`. Fermat's little theorem over the public + /// exponent `p-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..4).rev() { + let limb = P_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } +} + +impl PartialEq for Sm2FieldElement { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..4 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for Sm2FieldElement {} + +/// Schoolbook widening multiply of two 4-limb (256-bit) operands into an 8-limb (512-bit) +/// product. Not expressed generically over `Nat`: stable Rust has no way to bound an output +/// array's length to `2*L` for a generic `L`, so each curve's field module writes its own +/// fixed-width widening multiply (the "field arithmetic is per curve" rule the crate's design +/// notes describe for the reduction, applied here too). +/// +/// Carry propagation always walks every remaining output limb for a given row, whether or not the +/// carry is actually still nonzero: the loop bound depends only on the (public) row index `i`, not +/// on the (potentially secret) operand values, so this does not leak timing through an +/// early-exit carry chain. +fn widening_mul(a: &[u64; 4], b: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + for i in 0..4 { + let mut carry: u128 = 0; + for j in 0..4 { + let idx = i + j; + let prod = (a[i] as u128) * (b[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + let mut k = i + 4; + while k < 8 { + let s = (result[k] as u128) + carry; + result[k] = s as u64; + carry = s >> 64; + k += 1; + } + } + result +} + +/// The 32-bit word `A_i` of the 512-bit product, numbered as the module docs' derivation does: +/// `A = (A15 || A14 || ... || A0)`, each `A_i` a 32-bit integer, `A0` least significant. +fn word(t: &[u64; 8], i: usize) -> u64 { + (t[i / 2] >> (32 * (i % 2))) & 0xffff_ffff +} + +/// Zero-extends a 4-limb value to 5 limbs, for arithmetic against the 5-limb accumulator. +fn widen(v: &[u64; 4]) -> [u64; 5] { + [v[0], v[1], v[2], v[3], 0] +} + +/// `small * v`, one limb times a 4-limb value, as 5 limbs. `small` is [`reduce`]'s accumulator top +/// limb: a function of the operands, bounded in `[4, 18]`, and only ever a multiplicand here -- +/// the loop's trip count and memory accesses do not depend on it. +fn mul_small(small: u64, v: &[u64; 4]) -> [u64; 5] { + let mut out = [0u64; 5]; + let mut carry: u128 = 0; + for i in 0..4 { + let prod = (small as u128) * (v[i] as u128) + carry; + out[i] = prod as u64; + carry = prod >> 64; + } + out[4] = carry as u64; + out +} + +/// Schoolbook squaring of a 4-limb value into its 8-limb square. +/// +/// `a * a` is symmetric: the product `a_i * a_j` appears twice for every `i != j`. Forming each of +/// those once and doubling costs `L(L+1)/2` limb multiplications -- 10 at this width against +/// the 16 [`widening_mul`] would form for the same value. +/// +/// Three passes: the off-diagonal products `a_i * a_j` for `i < j`; a doubling of the whole +/// accumulator; then the diagonal squares `a_i * a_i` added in at limb `2i`. Writing `result[i + +/// 4]` in the first pass is an assignment rather than an accumulation because row `i` only ever +/// reaches limbs `2i + 1 ..= i + 4 - 1`, and no earlier row reaches limb `i + 4` either, so that +/// limb is still zero when the row's final carry lands on it. The doubling cannot overflow: the +/// off-diagonal sum is strictly less than `a^2 / 2`. +/// +/// Verified against Python's arbitrary-precision `**2` over 20,000 random 4-limb values plus the +/// all-ones worst case before being written here. +fn widening_square(a: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + + // Off-diagonal products, each formed once. + for i in 0..4 { + let mut carry: u128 = 0; + for j in (i + 1)..4 { + let idx = i + j; + let prod = (a[i] as u128) * (a[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + result[i + 4] = carry as u64; + } + + // Every off-diagonal product appears twice in the square. + let mut carry = 0u64; + for limb in result.iter_mut() { + let next_carry = *limb >> 63; + // `*limb << 1` always leaves bit 0 clear and `carry` is only ever 0 or 1, so the two + // operands are disjoint: mutating this `|` to `^` is an accepted equivalent, not a bug. + *limb = (*limb << 1) | carry; + carry = next_carry; + } + debug_assert_eq!(carry, 0, "doubling the off-diagonal sum overflowed 8 limbs"); + + // Diagonal squares. + let mut carry: u128 = 0; + for i in 0..4 { + let square = (a[i] as u128) * (a[i] as u128); + let low = (result[2 * i] as u128) + ((square as u64) as u128) + carry; + result[2 * i] = low as u64; + let high = (result[2 * i + 1] as u128) + (square >> 64) + (low >> 64); + result[2 * i + 1] = high as u64; + carry = high >> 64; + } + debug_assert_eq!(carry, 0, "adding the diagonal overflowed 8 limbs"); + + result +} + +/// Reduces an 8-limb (512-bit) value modulo `p`, per the term table derived in the module docs: +/// +/// ```text +/// B = T + S1 + S2 + ... + S13 - D1 - D2 - D3 - D4 (mod p) +/// +/// T = ( A7 || A6 || A5 || A4 || A3 || A2 || A1 || A0 ) +/// S1 = ( A8 || A11 || A10 || A9 || A8 || 0 || A9 || A8 ) +/// S2 = ( A9 || A14 || A13 || A12 || A11 || 0 || A10 || A9 ) +/// S3 = ( A10 || A15 || A14 || A13 || A12 || 0 || A11 || A10 ) +/// S4 = ( A11 || 0 || A15 || A14 || A13 || 0 || A12 || A11 ) +/// S5 = ( A12 || 0 || A15 || A14 || A13 || 0 || A13 || A12 ) +/// S6 = ( A12 || 0 || 0 || A15 || A14 || 0 || A14 || A13 ) +/// S7 = ( A13 || 0 || 0 || 0 || A15 || 0 || A14 || A13 ) +/// S8 = ( A13 || 0 || 0 || 0 || 0 || 0 || A15 || A14 ) +/// S9 = ( A14 || 0 || 0 || 0 || 0 || 0 || A15 || A14 ) +/// S10= ( A14 || 0 || 0 || 0 || 0 || 0 || 0 || A15 ) +/// S11= ( A15 || 0 || 0 || 0 || 0 || 0 || 0 || A15 ) +/// S12= ( A15 || 0 || 0 || 0 || 0 || 0 || 0 || 0 ) +/// S13= ( A15 || 0 || 0 || 0 || 0 || 0 || 0 || 0 ) +/// D1 = ( 0 || 0 || 0 || 0 || 0 || A8 || 0 || 0 ) +/// D2 = ( 0 || 0 || 0 || 0 || 0 || A9 || 0 || 0 ) +/// D3 = ( 0 || 0 || 0 || 0 || 0 || A13 || 0 || 0 ) +/// D4 = ( 0 || 0 || 0 || 0 || 0 || A14 || 0 || 0 ) +/// ``` +/// +/// Both call sites, `mul` and `square`, pass the product of two canonical (`< p`) field elements, +/// so `t < p^2` as in SP 800-186 §G.1's analogous NIST tables ("given an integer A less than +/// m^2"); the bounds below do not need that, and hold for any 8-limb `t`. +/// +/// Bounds. Each term is a 256-bit value, so `T + S1 + ... + S13 < 14*2^256`; the four `D` words +/// all sit at bits 64-95, so `D1 + ... + D4 < 4*2^96`. Any bias of `2^98` or more therefore keeps +/// the accumulator non-negative; `5p` is used because it also keeps the top-limb bound a clean +/// 18: `5p + 14*2^256 < 19*2^256`, so five limbs always suffice. (`sm2_field_tests.rs` drives the +/// limb through 4 to 17 on products; only non-product input, such as all ones, reaches 18.) +/// Folding that limb +/// back in via `2^256 ≡ C` adds `u_hi*C < 19*2^225 < 2^230`, leaving `V < 2^256 + 2^230`; since +/// `p > 2^256 - 2^225`, `V - p < 2^230 + 2^225 < p`, so exactly one conditional subtraction +/// finishes the job. +fn reduce(t: &[u64; 8]) -> [u64; 4] { + let a = |i: usize| word(t, i) as i64; + + // The term table in the module docs (`T + S1 + ... + S13 - D1 - D2 - D3 - D4`), one 32-bit + // column at a time: column `w` is `A_w` plus the words the `S` rows place at position `w`, + // minus the words the `D` rows place there, read straight down that table. + // + // Reading the table by columns instead of by rows is the whole optimisation: each of the eight + // 32-bit output words is a small signed sum of at most fourteen input words (fits an `i64` + // with room to spare), and the carries between words are resolved once, below, instead of + // once per row in a 5-limb addition. The value is identical to the row-wise sum, so the + // bounds the module docs prove for it -- and the `5p` bias they need -- are unchanged. + // Measured: a field multiplication fell from 39.1 ns to 21.6 ns, level with P-256's, whose + // nine-row table is small enough that the same rewrite made no measurable difference there + // (20.4 vs 21.7 ns) -- so P-256 keeps SP 800-186's row order and this curve, with eighteen + // rows, does not. + let columns: [i64; 8] = [ + a(0) + a(8) + a(9) + a(10) + a(11) + a(12) + 2 * a(13) + 2 * a(14) + 2 * a(15), + a(1) + a(9) + a(10) + a(11) + a(12) + a(13) + 2 * a(14) + 2 * a(15), + a(2) - a(8) - a(9) - a(13) - a(14), + a(3) + a(8) + a(11) + a(12) + 2 * a(13) + a(14) + a(15), + a(4) + a(9) + a(12) + a(13) + 2 * a(14) + a(15), + a(5) + a(10) + a(13) + a(14) + 2 * a(15), + a(6) + a(11) + a(14) + a(15), + a(7) + a(8) + a(9) + a(10) + a(11) + 2 * a(12) + 2 * a(13) + 2 * a(14) + 3 * a(15), + ]; + + // `V = sum(columns[w] * 2^(32 w))`, assembled as a two's-complement 5-limb value with one + // signed carry pass: `cur >> 32` is an arithmetic shift, so a negative running sum carries a + // negative amount into the next word, and the final carry (which can be negative) becomes the + // sign-extended top limb. `V` itself may be negative here -- the `D` words can exceed the + // `S` words in column 2 -- which is what the `5p` bias is for. + let mut v = [0u64; 5]; + let mut carry: i64 = 0; + for w in 0..8 { + let cur = columns[w] + carry; + v[w / 2] |= ((cur as u64) & 0xffff_ffff) << (32 * (w % 2)); + carry = cur >> 32; + } + v[4] = carry as u64; + // `V + 5p`, wrapping: the docs prove `V > -4 * 2^96`, so the true sum is non-negative and + // below `19 * 2^256`, and the wrapping add of two 5-limb values recovers it exactly. + let (acc, _) = nat::add(&v, &FIVE_P_LIMBS); + + // Fold the accumulator's top limb back in: `2^256 ≡ C (mod p)`. + debug_assert!(acc[4] <= 18, "SM2 reduction top limb exceeded its proven bound"); + let low: [u64; 4] = [acc[0], acc[1], acc[2], acc[3]]; + let (fold, carry) = nat::add(&mul_small(acc[4], &C_LIMBS), &widen(&low)); + debug_assert_eq!(carry, 0, "reduction fold overflowed 5 limbs"); + + let (diff, borrow) = nat::sub(&fold, &widen(&P_LIMBS)); + let mut selected = [0u64; 5]; + ct::conditional_select(Condition::::from_lsb(borrow), &fold, &diff, &mut selected); + debug_assert_eq!(selected[4], 0, "reduction left a value >= 2^256"); + [selected[0], selected[1], selected[2], selected[3]] +} diff --git a/crypto/ec/src/sm2_comb.rs b/crypto/ec/src/sm2_comb.rs new file mode 100644 index 000000000..2af77b45b --- /dev/null +++ b/crypto/ec/src/sm2_comb.rs @@ -0,0 +1,89 @@ +//! Fixed-base comb scalar multiplication: `[k]G` for SM2's base point `G`, with `k` secret. +//! Identical algorithm to [`crate::p256_comb`] -- see that module's docs for the full derivation +//! and verification methodology -- with SM2's own 256-bit order giving the same `D = 43`, +//! `FULL_COMB = 258` (zero-padded) shape as P-256's own case, and SM2's types substituted. +//! +//! # The comb +//! +//! For width `w = 6` and the scalar's bit length `BITS = 256` (SM2's order's bit length, +//! `draft-shen-sm2-ecdsa-02` Appendix D), `D = ceil(BITS / w) = 43` and `FULL_COMB = D * w = 258` +//! (two bits more than `BITS`, zero-padded). [`COMB_TABLE_X`]/[`COMB_TABLE_Y`] (in +//! [`crate::sm2_comb_table`]) hold, at index `i`, the point `sum_{bit b set in i} 2^(b*D) * G` -- +//! i.e. index `i`'s table entry encodes a width-6 "comb digit" as a subset sum of the doublings +//! `2^(0*D)*G, 2^(1*D)*G, ..., 2^(5*D)*G`. +//! +//! [`comb_multiply_base_point`] processes `D` rounds; round `i` reads the comb digit made of bits +//! `top-i, top-i-D, top-i-2D, ..., top-i-5D` of `k` (`top = FULL_COMB - 1`), doubles the running +//! accumulator, and adds the table entry for that digit. Every one of the `D*w = FULL_COMB` bit +//! positions is read by exactly one (round, digit-position) pair (verified, not checked in, by +//! confirming the six per-round ranges tile `0..FULL_COMB` with no gaps or overlaps), and every +//! step -- bit extraction, the table lookup, the add -- is branch-free in `k`'s value: the *bit +//! positions* read are fixed by the (public) loop structure, and [`table_lookup`] scans every +//! entry under a mask rather than indexing by the secret digit. +//! +//! Verified (not checked in) against 200 random `k` plus edge cases (`k = 1, 2, 3, n-1`), each +//! cross-checked against the standard affine group law (`a = -3`) via repeated doubling and +//! conditional addition (a world apart from the comb, so this isn't just checking the algorithm +//! against itself). + +use crate::sm2::Sm2FieldElement; +use crate::sm2_comb_table::{COMB_TABLE_X, COMB_TABLE_Y}; +use crate::sm2_point::Sm2JacobianPoint; +use crate::sm2_scalar::Sm2Scalar; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +const WIDTH: usize = 6; +const BITS: usize = 256; +const D: usize = BITS.div_ceil(WIDTH); +const FULL_COMB: usize = D * WIDTH; +const TABLE_SIZE: usize = 1 << WIDTH; + +/// `[k]G`, branch-free in `k`. +pub fn comb_multiply_base_point(k: &Sm2Scalar) -> Sm2JacobianPoint { + let limbs = k.limbs(); + let mut r = Sm2JacobianPoint::INFINITY; + let top = (FULL_COMB - 1) as isize; + for i in 0..D { + let mut secret_index: usize = 0; + let mut j = top - i as isize; + while j >= 0 { + let bit = if (j as usize) < BITS { + (limbs[(j as usize) / 64] >> ((j as usize) % 64)) & 1 + } else { + 0 + }; + secret_index = (secret_index << 1) | (bit as usize); + j -= D as isize; + } + r = r.double(); + let (x, y, is_infinity) = table_lookup(secret_index); + r = r.add_affine(&x, &y, is_infinity); + } + r +} + +/// Looks up comb table entry `secret_index`, scanning every one of the [`TABLE_SIZE`] entries +/// under a mask so the index never steers which memory is read. See +/// [`crate::p256_comb::table_lookup`]'s docs for the cache-safe lookup pattern this mirrors. +fn table_lookup(secret_index: usize) -> (Sm2FieldElement, Sm2FieldElement, Condition) { + let mut x_limbs = [0u64; 4]; + let mut y_limbs = [0u64; 4]; + for i in 0..TABLE_SIZE { + let is_this_entry = Condition::::is_equal(i as u64, secret_index as u64); + let mut next_x = [0u64; 4]; + ct::conditional_select(is_this_entry, &COMB_TABLE_X[i], &x_limbs, &mut next_x); + x_limbs = next_x; + let mut next_y = [0u64; 4]; + ct::conditional_select(is_this_entry, &COMB_TABLE_Y[i], &y_limbs, &mut next_y); + y_limbs = next_y; + } + // Entry 0 is the point at infinity; every other entry is affine (Z == 1). The accumulator's + // `add_affine` takes the identity as a flag rather than as a `Z == 0` coordinate. + let is_infinity = Condition::::is_equal(secret_index as u64, 0); + ( + Sm2FieldElement::from_internal_limbs(x_limbs), + Sm2FieldElement::from_internal_limbs(y_limbs), + is_infinity, + ) +} diff --git a/crypto/ec/src/sm2_comb_table.rs b/crypto/ec/src/sm2_comb_table.rs new file mode 100644 index 000000000..932f2f6a7 --- /dev/null +++ b/crypto/ec/src/sm2_comb_table.rs @@ -0,0 +1,206 @@ +//! The checked-in fixed-base comb table for `[k]G` (SM2's base point `G`), width 6, matching +//! this crate's other 250-bit-and-up curves' choice. See [`crate::sm2_comb`] for the multiplier +//! that uses this table and the algorithm it implements. +//! +//! `COMB_TABLE_X[i]`/`COMB_TABLE_Y[i]` are the affine `(x, y)` coordinates of the `i`-th table +//! entry, as little-endian `u64` limbs; entry `0` is the point at infinity (`x = y = 0`, which is +//! not a valid affine coordinate pair for any point on the curve, so it's an unambiguous sentinel +//! here). +//! +//! Regenerated and compared against these checked-in values by this module's own `tests` +//! submodule below, using the same construction a (not checked in) scratch example used -- so a +//! hand-edit or transcription error in this file cannot survive `cargo test`. A unit test rather +//! than an integration test because the table is deliberately `pub(crate)`, not exposed outside +//! the crate: see [`crate::p256_comb_table`]'s docs for the same reasoning. + +pub(crate) const COMB_TABLE_X: [[u64; 4]; 64] = [ + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0x715a4589334c74c7, 0x8fe30bbff2660be1, 0x5f9904466a39c994, 0x32c4ae2c1f198119], + [0x30297f640172d318, 0xef6784ec9a477091, 0x44ef2ee6203f324f, 0xbddbb72e16f0c0ad], + [0xdc3f9e9b90b304b2, 0x30575d04556bd5d2, 0x82db8385293b4962, 0x5dec1d201cfa7326], + [0xbadf32318c218204, 0xc915b83030bee951, 0x08172c75e8fcd723, 0xc3fd77aaa834c8d7], + [0x6ee0babad9e3df53, 0x2dfb517e81c0e352, 0x3d885b4eb185a8ae, 0xd1869b3fff5b3e6d], + [0x5d910dc1936657f3, 0x0d6fa7f347354660, 0x7a8f622d44777f3b, 0x741cd4ae0e5e38f8], + [0xc2485e8957ae5ed2, 0x67c9ea4f8d42b904, 0x87fc85a61d8892b1, 0x6ed429a86861c277], + [0x9bf3db7629176ddf, 0xd53e73af2990e129, 0x5caf459b22655702, 0x0b5aa6d9a4f3b961], + [0x753675f22de491d6, 0xa55af38d801b8cce, 0xc08a004724056e61, 0x40ad97d9b2054e97], + [0x1b30ee28a04adc5a, 0x4935f0e4dafc4cc7, 0x57abf49417a7dc5d, 0xb5b309c6a577c8ae], + [0x2ecd57f2a8aebddb, 0xd0ada976e4160d1d, 0x983f8cb2fa643a27, 0x5366a15bb08ff503], + [0x6fb3ceb12a2853fa, 0x63487ad721bb5995, 0xe85585dd7379c552, 0xbec9c6fd14f20cc8], + [0x9444752a1198636c, 0x11724e36c14c1faa, 0x029570bce8237ddc, 0x29100fff4a0bd836], + [0x90a5816e926fa17b, 0xd93cf4db513e48c4, 0x0d78ef3c7228cb7e, 0x03178e917583d056], + [0xd6178587f20efb49, 0x9dba2f8c521fc1cb, 0x66bf7dd8265498bd, 0xd31be05182310ab3], + [0xc897a6b93429783d, 0xb5d6b5765e42dc7a, 0xa5eee9d4a635ff82, 0x06687d1078ba0903], + [0x0c022220a560e0ec, 0x40e69eb3fa9a8e9c, 0x21ef2cee9322ac95, 0x8ef2fea54288855b], + [0x08b54030bdad5bb8, 0x7679994e7f010416, 0x39d782b377963b74, 0x72863b2848bb975c], + [0x72174d6352073294, 0x353bec8490520ed0, 0xf2c6592776d36e67, 0x46f2d836da2a1cef], + [0x34e2c829eaeb2769, 0xb2bb63ceabab6d62, 0x156ab43cfbf7e8b7, 0x6f88c5d3870fda46], + [0x20139aef6baf953c, 0x75181c03ad9c7d4d, 0x929e86aed802aee5, 0x1bc853c658824ff4], + [0xed7e355357b93b75, 0x3d87d0f5d9585b93, 0x3c516e063c8eb8c1, 0xc1801708a36ec12c], + [0xcd5b2e5c784bd7be, 0x09a8bbbbf058d564, 0x6326c52bdc6bb6fc, 0xdf084bfa71ba22fc], + [0x51c3de76d5113448, 0xb13a7919d9559ec7, 0xb465fb3576c7d63c, 0xa0a79bcb8e20b3fe], + [0xa4c19824f806e454, 0x56cbda20382be57c, 0x961e17f2ee0db436, 0x3b21e2cbf460be30], + [0xce511bf8e92252a7, 0x515db7d068550f8c, 0x93581561ea437b0f, 0x36dad43db0097efe], + [0x263c68c8124af0d7, 0x3b02064371b60c09, 0xa97f1ac7e87eb620, 0xe04240dfde0bf19f], + [0x126e9488176ac6a2, 0x014348a239083dd4, 0x51d230b8b0c290e4, 0x73007c218684ea44], + [0xcc45d4b848946c45, 0xd5859cbd0b7a8a64, 0x0b0adf45a7434187, 0x6f620d4ec14c1857], + [0x77e632ec0866c96a, 0xd5e344e5a52f1a43, 0x482f72079cdeab8f, 0x5d499c6d6ed92a4d], + [0x2d6cdb59b7cf18dd, 0x45ab5696e3788f28, 0x78fe14a98fda73aa, 0x260c3de859294931], + [0xf8e996c937040556, 0x965ab458322fdcb2, 0x4a21d0a2b9411d1d, 0x20dc3a017e6b3e61], + [0x72a383a2c79a77d5, 0x05a6ad88a0e0605f, 0xd681b2e984c9c838, 0x20b96ae6a9fde8c6], + [0x3e3f57f2232c6f08, 0x809b47f0bb093ebc, 0x0dafe0b2263f9631, 0x0d6d881baaa41540], + [0x87d0a8abd7f4a968, 0xeed3035ea73ac891, 0xbd3ae0887bee9225, 0xfac16b813f440f26], + [0xfcf868f82e2d7e27, 0x54d19b6afaaa750c, 0xec3403d09dec75ef, 0x928f0bb1e5018ccd], + [0x9593e2fb1ada0120, 0x098b2676b5b292df, 0x63c10b1b02cfcd0f, 0xefffbb5f9ca7fdb3], + [0x93d1a976962e79b4, 0xbd61c14f485b117f, 0xb3852db296ac32c3, 0x7bd08c5bbaecf1e2], + [0xbdbd8499c54a069a, 0x9ab6f46c8d7d7332, 0xb08af5f213c6d9d0, 0xf651bb9a2fae8ceb], + [0xe13ae537f0d74a60, 0xfc5c71c21419b747, 0x9dbf20e1aaa80c3b, 0x6b823ec2b8c0bef3], + [0xc44582445e5f1c06, 0x8ded9fd756aa4d8b, 0xf19c393c666b59ed, 0xfc0da5d731fdc4d6], + [0x3efc2b1519ab611f, 0x6bfec55600a04baa, 0x504896fe549b315d, 0xd745be971421d08b], + [0x86da9e03d5d9ad8f, 0x562edd8f08957753, 0xe0bc92bee76e48cd, 0x362323599a5a81cf], + [0x40ff530c79e067a3, 0xde0ec69b091056f0, 0x2c4f093879282347, 0x95b4758cc5803244], + [0xc341ab9154c886ee, 0x54ae000c6cadd610, 0xe923576b2159a153, 0x18a50eb80d506782], + [0x0c990bbb3237f366, 0x144647a2b61eb0b4, 0x9e17ab8826b55245, 0x7adf2798ac48e621], + [0x337f8816ac9b1c43, 0xb71cd27f13454b35, 0xab83576f76147dd0, 0x20d371742deab886], + [0xc2ef0dee3cd36c73, 0xe9fe773bacd0610e, 0x3d2c02501f0fdd2c, 0x96209cfa241789b9], + [0x6f6c11c626e528be, 0xc78c24771d9bd9d1, 0xbe87718b1c154eeb, 0x95d9391f216b8021], + [0x3d5bfe5802e930c3, 0x74a7234cd495ee2c, 0x3bb4c90912ac6b1c, 0x20041f7006d29f96], + [0xbaf850bf0f429ebb, 0x60fec0f965a5e199, 0xb52a1c2d73c9808a, 0x9656e740e6b54848], + [0x5cf53085cc4d2ede, 0xce4c2911fc77ee11, 0x828feae65f7134d2, 0x914a848e5673024a], + [0xff925e20b53b4662, 0xa628335e0805770f, 0xae493b9409b175d8, 0x0191af58e356cf1c], + [0xd1a82d4aeb85d4d9, 0xd9dbe53f79884646, 0x924c98d3a0b12bc0, 0xbcb12cee0d7da4fb], + [0xd9cb4009613f1856, 0xdd61c6e146f7c005, 0x9acc40ec12d25a8c, 0x8e775e0175d964cc], + [0x88625cad773d76e6, 0x91511a7494c425f3, 0x1aaf33c002dc65da, 0xb4db03cb61c6b4b4], + [0xa8cfac075abe26a3, 0x107a785ab5b350c2, 0x9cd34055316aea68, 0x8fb56dd1691864b8], + [0x51b5893000755844, 0x2627e603b5146640, 0xc40cd27135fd24ef, 0xc53979576e2f9996], + [0x71633d18db7c39ef, 0x8f2ac0cb16cd69f8, 0x499f9fc01be7799d, 0xb810e071bb5cd0d7], + [0x50b1a14d547f6ba1, 0xc50e86c62fb93522, 0x7e00b050a77cb534, 0xbf8cec2ce55aef6a], + [0x64730071e3921016, 0x572b05bb9846b7c5, 0x609c96317455b218, 0x0d33be90d266965f], + [0x3119de20ebc172ac, 0x078e7cc0d524894b, 0xbc8bd2119d638830, 0x48cb13104b525dda], + [0xd1e7453e1e59bc8e, 0x7f0541628a9574c4, 0xfb5fac573ed25e17, 0x7c8c4433518025ef], +]; + +pub(crate) const COMB_TABLE_Y: [[u64; 4]; 64] = [ + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0x02df32e52139f0a0, 0xd0a9877cc62a4740, 0x59bdcee36b692153, 0xbc3736a2f4f6779c], + [0x7d748dfb2957f9bb, 0x17a4fdca60443ba0, 0x67c440e8bc64c48c, 0x58bf85f5bbb9c432], + [0x11e35829dd62dd65, 0x1b5f64616eb50a12, 0xacc4aba748697535, 0x3a3b657302465f1a], + [0x4a3759686ddd1a8d, 0xd17459393ef8dfd0, 0x9eb6112e0f059ca5, 0x419a3cb9895b4a54], + [0x2d76ffa5c396f9b0, 0xc1f7f09d121ae36b, 0x68fd44137ca2d1ca, 0x47aec31d3d5c2644], + [0xa545b0fd992fae9e, 0x70e8ccd2752cb56d, 0x713be4a54a682331, 0x86ace92ce4656e50], + [0xc698accb967977d0, 0x7cd12a528f10add1, 0x9cbb726b81b65ac1, 0x9f2e6edf8f19b983], + [0xf08bf17067ab7ab4, 0xb47d3dfc9919b007, 0xc99b1c7de482791e, 0x01c94fe169f7cd30], + [0xf392463627849851, 0x7b76cf052c8c2b87, 0x10727fae95e1ae8c, 0x98685c7df0c04a43], + [0x18920974aa57143f, 0x38f4a412e5d0db43, 0x1cd16fd447c6a2e6, 0x2cf9038f8b921ee1], + [0xcdf2b3461b728fd2, 0xa5216f7f58067224, 0x17b48358379a8651, 0xa54cad0b2f6a39e1], + [0xc43a62c585503137, 0xd58c9c4a502c211e, 0x9162a6b8a162bdca, 0xc660365ca30d71b3], + [0xc087a4767c73811a, 0xfd68b2636110d7c5, 0x81ee840404a88143, 0x7a84a51f8fc875dc], + [0xb3f9a3a0d11c1f17, 0xf16584b911c6dc86, 0x3bcd5ef97419b437, 0xeb8c287fb1cd1b2f], + [0x46b412844af6fa75, 0xb9eb5d28e023c3ae, 0x2c3c3a9524e64265, 0xb9090d54aace33a2], + [0x036e4394ee352a7f, 0xa8133f73ada63db3, 0x1a7c23ce9652df72, 0x61a557d8490c51dd], + [0xbe9d68a91498ea63, 0xb2b9c52c58d671a1, 0x17925255a842f06f, 0x404837ce7f6769c3], + [0x8230861d98741357, 0xad6fbc514dd4e7bb, 0x733ab9a4322107c7, 0xa4fd30f1a4c97421], + [0x0377fc6d3db7be51, 0x6b26f7746d733b22, 0x3d6f432bc01192f9, 0xc0aba70ce0b5ee0f], + [0x41a6bbdbc91cab81, 0x01a1b8ea36117413, 0x888ae651663e9731, 0x7746173c72f5fb73], + [0x4e4cfde197c46386, 0xc059f44c71177b9f, 0x23e2d11acec5c4ff, 0x4b80577fba0b8bd9], + [0xf5bf536c2ba4fc63, 0x26c1c3e7a9646c92, 0xf403ffe5a4f1845e, 0x41bc7859837e6b16], + [0x07a5b34046cd75cc, 0xd805bbf569579f5f, 0xfe5b0cec62085d99, 0xb56b7fd8539a97b9], + [0xa3400ff74599561c, 0x46c7b206f0b0dea7, 0x2c2a2bcdf1d83d3e, 0x0b7de6648d942253], + [0x14410812ef0180a4, 0xdbf936f5adf73405, 0xab7e1bf6f17f39a6, 0xdbbf77d39f5b7efb], + [0x0ebb1fc4ab517dc7, 0x1440ab816918b140, 0x22e8115f4fa40e9c, 0x608ba5a6654dfa89], + [0x857b3a697d8d6e9b, 0xa129e1729a4813bb, 0xa43e41765de86e0c, 0x8f6020ba36c190eb], + [0x4ca594ca8433f3ef, 0x14a39d1075d22f55, 0xca4985075f659fde, 0x9789ae39187c3019], + [0x79cf0ade6534e2e3, 0xd559c14b43929fad, 0x4a91bda3cb8e93b7, 0x86ca3d9a45ddabdd], + [0x738e3e3dfb8deef5, 0x143f245fee4f04fd, 0x83997f458fb0ff3d, 0x04a28a30225ee6f9], + [0x15a0ebdb9366890a, 0x7cd9bc2243fc7dad, 0x56dbe637a9cac5c8, 0xa07828d1a48e43b2], + [0x143a57f20804d010, 0x0d18f09a537888a1, 0x085914296f7ceef0, 0x2bd03eec6509d5f1], + [0x8ccec33be0a1dd96, 0x345f390bce4b2ddc, 0x742d263ee159f68c, 0x34d73d523a08adcf], + [0x56eb4f425cf2b73d, 0xf4b163e56e50b55d, 0xe14a9150fa0305c0, 0x9785727f37c59c6c], + [0x5a70cd7a972a5abe, 0x29f02f071d2701a6, 0x3ef174da781d95ad, 0xb95a5bd20276090b], + [0x00cda4813063c68b, 0x6229d0786f28ae76, 0x0f927abfec0435b1, 0x9c37e1160ce7580b], + [0xe694cd53342755b9, 0x43bb553e9fb29f7d, 0x792fb9debd7d70a6, 0xe160f12e1d05b504], + [0x3000324bce86aedb, 0x9ba0c6d2e5f6eccf, 0x4a607fd220df4e55, 0x6c52198024e94c00], + [0x54570c536795109c, 0xd484dba50c8558b8, 0xa21a49f531ff65d2, 0x58686a5f875ff04f], + [0x6ee66dfcdcb90950, 0x7b820122a5c07d74, 0xe87135924528a3b1, 0xb09a50e1e2eb4f2f], + [0x77d359ebc87fbf4f, 0x93964d1047168f26, 0xdac2730cd8755e03, 0x8dc3d0e4f160d179], + [0x4837a1a14b89c855, 0xb2174cc98115f228, 0xb174667749595bb8, 0xb81aee315a0ed4db], + [0xea0c48076598b02e, 0x7d0c5c8730b46a5b, 0xb962395e48492e0d, 0x79d728fbb76242b9], + [0xa84bef0712d0cdf4, 0xa212c8c82d71c438, 0x5adb8aa4ceb59a4e, 0x1f8a3543416aa8d6], + [0x78cb15480c85466b, 0x42acb71c101a17c8, 0x10da8babea9b27c7, 0x8bab6db5c4bded15], + [0xa49ed2f7333a7fa0, 0xc4fbd4d8af8c2793, 0xb610d7a59a6a81d1, 0xefe279db2499b1a2], + [0xe5e4cbd94d62f32a, 0x81feae7696260c18, 0x75eb41344b0d1973, 0xcbd8f6bdf79adb68], + [0xd5dcb13ac47e261b, 0x0c2de49f70cabee5, 0xa52b44b933b8b580, 0x2436c98dd252516b], + [0xe58d49b2b2896dfe, 0x5abe4bb1c7508945, 0x53b3ec3f8313782e, 0xf423f6e7dcacfead], + [0x161e1ec2b2fc689a, 0x82f0a6ff6dcf3667, 0x0e724a76846ff153, 0xc53969eacbca9411], + [0x214f504c1d7c8da2, 0x0777e406f44e9454, 0x96f834787b40f380, 0x9c065faf561c29c6], + [0x3cc2e0a2d17ca7ab, 0xa81528877a79c99f, 0xc2cdcd3ff8b499ac, 0x5a333a3b39026531], + [0xef0534400cc19085, 0x8ee09a42c64c2daa, 0x94e208975a8e84b0, 0xb808ee9f7800e89e], + [0x484d72b85df5cb8c, 0x5a75ff19019bb2b0, 0x7fa879a8fbd82fab, 0xf5c56db20f37300a], + [0x103880ae2cd7e0f8, 0xb1e55dc0de64187c, 0xfbd460818931057a, 0xd2d4a3a0134df8b5], + [0x9dbd51ad0c9b9d07, 0xaeb88b187f2d27db, 0xd213ada3f6e2c7ac, 0x6b5b5a5815241dca], + [0xaa41697e267da38d, 0x0dafdd8460455ec3, 0x82c56124fbfe5b9f, 0x8ee688f8ec2f6b2c], + [0x50a6d2bada74067b, 0x177d55481c602d6e, 0xfaa2d230bf89412c, 0xb2e11b1af5c6073f], + [0x52c7864a9c416c95, 0x2e12f61de2fa70fd, 0x627722935b7cabba, 0x00685ecdfad27705], + [0xd794cd1077b9ca0b, 0x0c5fea2983fdc35c, 0x729492cdd2c5ad2e, 0x31feb3cf5dffbe9c], + [0xa42b2668d30d1ae0, 0xec5daa69f8efecc0, 0x60c24211b430b4b6, 0xcd121826ac0b5871], + [0x1e7019dfcdec72c1, 0xee69d284d5ba27e1, 0x4407da0bd774691d, 0xd3b1bf326c3abdd6], + [0xead550d29cbe50a7, 0xa132a509dee07e44, 0xeb491785859f17cd, 0xc75df76213e683d5], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::sm2::Sm2FieldElement; + use crate::sm2_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::sm2_point::Sm2JacobianPoint; + + const WIDTH: usize = 6; + const BITS: usize = 256; + const D: usize = BITS.div_ceil(WIDTH); + const TABLE_SIZE: usize = 1 << WIDTH; + + /// Regenerates the comb table from scratch via the crate's own (independently verified) + /// point arithmetic, per this module's construction: `pow2[i] = 2^(i*D) * G`, then + /// `table[idx] = sum of pow2[b] for each bit b set in idx`. + #[test] + fn regenerated_table_matches_checked_in_constants() { + let g = Sm2JacobianPoint::from_affine( + Sm2FieldElement::from_limbs(G_X_LIMBS), + Sm2FieldElement::from_limbs(G_Y_LIMBS), + ); + + let mut pow2 = [g; WIDTH]; + for i in 1..WIDTH { + let mut p = pow2[i - 1]; + for _ in 0..D { + p = p.double(); + } + pow2[i] = p; + } + + let mut table = [Sm2JacobianPoint::INFINITY; TABLE_SIZE]; + for bit in (0..WIDTH).rev() { + let step = 1usize << bit; + let pw = pow2[bit]; + let mut i = step; + while i < TABLE_SIZE { + table[i] = table[i - step].add(&pw); + i += 2 * step; + } + } + + for (idx, point) in table.iter().enumerate() { + if idx == 0 { + assert!(point.is_infinity().to_bool(), "table[0] must be infinity"); + assert_eq!(COMB_TABLE_X[0], [0, 0, 0, 0]); + assert_eq!(COMB_TABLE_Y[0], [0, 0, 0, 0]); + } else { + let (x, y) = point + .to_affine() + .unwrap_or_else(|| panic!("regenerated table[{idx}] is unexpectedly infinity")); + assert_eq!(x.to_limbs(), COMB_TABLE_X[idx], "table[{idx}].x mismatch"); + assert_eq!(y.to_limbs(), COMB_TABLE_Y[idx], "table[{idx}].y mismatch"); + } + } + } +} diff --git a/crypto/ec/src/sm2_domain.rs b/crypto/ec/src/sm2_domain.rs new file mode 100644 index 000000000..7c94d0503 --- /dev/null +++ b/crypto/ec/src/sm2_domain.rs @@ -0,0 +1,20 @@ +//! SM2 domain parameters, `draft-shen-sm2-ecdsa-02` Appendix D. `p` and `a = -3` live in +//! [`crate::sm2`] (the `a = -3` shortcut is baked directly into +//! [`crate::sm2_point::Sm2JacobianPoint::double`]); `n` lives in [`crate::sm2_scalar`]. This +//! module holds the rest: the curve coefficient `b` and the base point `G`. +//! +//! Values extracted directly from a freshly downloaded copy of the draft (not retyped, not from +//! recall) and verified in Python: `G` is on the curve and `n·G` is the point at infinity, before +//! any of this was ported to Rust. + +/// The curve coefficient `b` in `y^2 = x^3 - 3x + b`, little-endian `u64` limbs. +pub const B_LIMBS: [u64; 4] = + [0xddbcbd414d940e93, 0xf39789f515ab8f92, 0x4d5a9e4bcf6509a7, 0x28e9fa9e9d9f5e34]; + +/// The base point `G`'s `x` coordinate, little-endian `u64` limbs. +pub const G_X_LIMBS: [u64; 4] = + [0x715a4589334c74c7, 0x8fe30bbff2660be1, 0x5f9904466a39c994, 0x32c4ae2c1f198119]; + +/// The base point `G`'s `y` coordinate, little-endian `u64` limbs. +pub const G_Y_LIMBS: [u64; 4] = + [0x02df32e52139f0a0, 0xd0a9877cc62a4740, 0x59bdcee36b692153, 0xbc3736a2f4f6779c]; diff --git a/crypto/ec/src/sm2_point.rs b/crypto/ec/src/sm2_point.rs new file mode 100644 index 000000000..3a1a1a1dd --- /dev/null +++ b/crypto/ec/src/sm2_point.rs @@ -0,0 +1,372 @@ +//! Branch-free Jacobian-coordinate point arithmetic on the SM2 curve `y^2 = x^3 - 3x + b`. +//! Identical formulas to [`crate::p256_point`] -- see that module's docs for the full derivation +//! and the branch-free exceptional-case-masking methodology, which applies unchanged here since +//! SM2's curve shares P-256's `a = -3` shortcut (see [`crate::sm2`]'s docs) -- with SM2's own +//! field type and domain constants substituted. +//! +//! A point `(X, Y, Z)` represents the affine point `(X/Z^2, Y/Z^3)`; `Z = 0` is the point at +//! infinity, represented canonically as `(1, 1, 0)`. +//! +//! [`Sm2JacobianPoint::double`] is the standard `a = -3` Jacobian doubling (`M = 3(X-Z^2)(X+Z^2)`, +//! `S = 4XY^2`, `T = 8Y^4`, `X3 = M^2 - 2S`, `Y3 = M(S - X3) - T`, `Z3 = 2YZ`), and +//! [`Sm2JacobianPoint::add`]'s generic-case formula is the standard Jacobian addition (`H = U1 - +//! U2`, `R = S1 - S2`, `X3 = R^2 + H^3 - 2V`, `Y3 = R(V - X3) - S1*H^3`, `Z3 = H*Z1*Z2`, where `U1 +//! = X1*Z2^2`, `S1 = Y1*Z2^3`, `U2 = X2*Z1^2`, `S2 = Y2*Z1^3`, `V = H^2*U1`). +//! +//! Verified (not checked in) against the standard affine group law using SM2's own domain +//! parameters, over 500 random on-curve points and their scaled (non-canonical-`Z`) Jacobian +//! representations, covering every exceptional case (`self`/`other` infinite, same point, opposite +//! point) plus the ordinary case, before any of this was ported here. + +use crate::sm2::Sm2FieldElement; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; + +/// A point on the SM2 curve in Jacobian coordinates. +#[derive(Clone, Copy, Debug)] +pub struct Sm2JacobianPoint { + /// `X`, the Jacobian `X` coordinate. + pub x: Sm2FieldElement, + /// `Y`, the Jacobian `Y` coordinate. + pub y: Sm2FieldElement, + /// `Z`, the Jacobian `Z` coordinate; `Z == 0` denotes the point at infinity. + pub z: Sm2FieldElement, +} + +impl Sm2JacobianPoint { + /// The point at infinity, the identity of the curve's group. + pub const INFINITY: Self = + Self { x: Sm2FieldElement::ONE, y: Sm2FieldElement::ONE, z: Sm2FieldElement::ZERO }; + + /// Builds a Jacobian point from affine coordinates (`Z = 1`). Does not check that `(x, y)` is + /// actually on the curve -- see [`crate::sm2_sec1`] for that. + pub fn from_affine(x: Sm2FieldElement, y: Sm2FieldElement) -> Self { + Self { x, y, z: Sm2FieldElement::ONE } + } + + /// TRUE iff this is the point at infinity. + pub fn is_infinity(&self) -> Condition { + self.z.is_zero() + } + + /// Converts to affine `(x, y)` coordinates, or `None` for the point at infinity. Not constant + /// time (branches on [`Self::is_infinity`]): intended for output/decoding boundaries on + /// already-public points, not for use on a secret intermediate value. + pub fn to_affine(&self) -> Option<(Sm2FieldElement, Sm2FieldElement)> { + if self.is_infinity().to_bool() { + return None; + } + let z_inv = self.z.invert(); + let z_inv2 = z_inv.mul(&z_inv); + let z_inv3 = z_inv2.mul(&z_inv); + Some((self.x.mul(&z_inv2), self.y.mul(&z_inv3))) + } + + /// `-self`: negating the `Y` coordinate, per SP 800-186 Appendix A.1.1 (`-P = (x, -y)`). + pub fn negate(&self) -> Self { + Self { x: self.x, y: self.y.negate(), z: self.z } + } + + /// `2 * self`, via the curve's `a = -3` Jacobian doubling shortcut. Total: correct (and, since + /// squaring/multiplying `0` is well-defined, does not panic) even when `self` is the point at + /// infinity, though the result is only meaningful when it is not (callers needing the + /// infinity case handled select for it separately, as [`Self::add`] does). + pub fn double(&self) -> Self { + let y1_squared = self.y.mul(&self.y); + let t = y1_squared.mul(&y1_squared); // Y1^4 + let z1_squared = self.z.mul(&self.z); + let t1 = self.x.sub(&z1_squared); + let m = self.x.add(&z1_squared); + let m = m.mul(&t1); // (X1 - Z1^2)(X1 + Z1^2) = X1^2 - Z1^4 + let m = m.add(&m).add(&m); // 3(X1^2 - Z1^4) == 3*X1^2 + a*Z1^4 for a == -3 + let s = y1_squared.mul(&self.x); + let s = s.add(&s).add(&s).add(&s); // 4*X1*Y1^2 + let t8 = t.add(&t).add(&t).add(&t).add(&t).add(&t).add(&t).add(&t); // 8*Y1^4 + let x3 = m.mul(&m).sub(&s).sub(&s); + let y3 = s.sub(&x3); + let y3 = y3.mul(&m).sub(&t8); + let z3 = self.y.add(&self.y).mul(&self.z); + Self { x: x3, y: y3, z: z3 } + } + + /// The generic (`self != ±other`, neither infinite) Jacobian addition formula, computed + /// unconditionally, plus the `H` and `R` intermediates [`Self::add`] uses to detect the + /// exceptional cases: `H == 0` iff `self` and `other` have the same affine `x`, and among + /// those, `R == 0` iff they are the same point (needs doubling) vs `R != 0` iff they are + /// negations of each other (sum is infinity). + fn generic_add(&self, other: &Self) -> (Self, Sm2FieldElement, Sm2FieldElement) { + let z2_sq = other.z.mul(&other.z); + let u1 = self.x.mul(&z2_sq); + let s1 = self.y.mul(&other.z).mul(&z2_sq); + let z1_sq = self.z.mul(&self.z); + let u2 = other.x.mul(&z1_sq); + let s2 = other.y.mul(&self.z).mul(&z1_sq); + + let h = u1.sub(&u2); + let r = s1.sub(&s2); + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&u1); + let g_neg = g.negate(); + let acc = s1.mul(&g_neg); // -S1*H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); // R^2 - (2V - H^3) == R^2 + H^3 - 2V + let y3_temp = v.sub(&x3); + let y3 = acc.add(&y3_temp.mul(&r)); // -S1*H^3 + (V - X3)*R + let z3 = h.mul(&self.z).mul(&other.z); + + (Self { x: x3, y: y3, z: z3 }, h, r) + } + + /// `self + other`, branch-free per the module docs: computes the generic-add and doubling + /// candidates unconditionally, then masks in the correct result for whichever of the four + /// exceptional cases (either operand infinite, same point, opposite point) applies, with no + /// branch on `self` or `other`. + pub fn add(&self, other: &Self) -> Self { + let self_is_infinity = self.is_infinity(); + let other_is_infinity = other.is_infinity(); + + let (generic, h, r) = self.generic_add(other); + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + + // Lowest priority first: each later select overrides the ones before it, so the final + // (highest-priority) case applied is "other is infinity", then "self is infinity". + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for an `other` given in **affine** coordinates (`Z2 = 1`), or the identity + /// when `other_is_infinity` is TRUE (`x2`/`y2` are then ignored). The same group law and the + /// same branch-free discipline as [`Self::add`] -- generic-add and doubling candidates computed + /// unconditionally, the exceptional cases masked in -- but with `Z2 = 1` the generic formula + /// simplifies: `U1 = X1`, `S1 = Y1` and `Z3 = H * Z1`, which drops five of its sixteen field + /// multiplications. The fixed-base comb multiplier adds a table entry to its accumulator on + /// every round, and every table entry is affine or the identity, so it is the caller this + /// exists for; `pub(crate)` because a caller has to know its point really has `Z = 1`. + pub(crate) fn add_affine( + &self, + x2: &Sm2FieldElement, + y2: &Sm2FieldElement, + other_is_infinity: Condition, + ) -> Self { + let self_is_infinity = self.is_infinity(); + + // The generic mixed addition, computed unconditionally: `U1 = X1`, `S1 = Y1`. + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2 + let r = self.y.sub(&s2); // S1 - S2 + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); // R^2 + H^3 - 2V + let y3 = acc.add(&v.sub(&x3).mul(&r)); // -S1*H^3 + (V - X3)*R + let z3 = h.mul(&self.z); // H * Z1 * Z2 with Z2 = 1 + let generic = Self { x: x3, y: y3, z: z3 }; + + let h_is_zero = h.is_zero(); + let r_is_zero = r.is_zero(); + let is_same_point = h_is_zero & r_is_zero; + let is_opposite_point = h_is_zero & !r_is_zero; + let doubled = self.double(); + let other = Self { x: *x2, y: *y2, z: Sm2FieldElement::ONE }; + + // Same priority order as `add`: the identity cases override everything else. + let mut result = generic; + result = select_point(is_opposite_point, &Self::INFINITY, &result); + result = select_point(is_same_point, &doubled, &result); + result = select_point(self_is_infinity, &other, &result); + result = select_point(other_is_infinity, self, &result); + result + } + + /// `self + other` for points that are **public**, branching on the exceptional cases instead + /// of computing every candidate and masking. + /// + /// [`Self::add`] must evaluate the doubling candidate on every call, because on secret inputs + /// it cannot branch on whether the doubling case applies -- that costs a full point doubling + /// (roughly a third of the addition) on every addition, whether or not it is ever used. This + /// version pays it only when the operands really are the same point, which for a scalar + /// multiplier over distinct precomputed multiples is essentially never. + /// + /// Restricted to `pub(crate)` and used only by [`crate::sm2_wnaf`], whose scalars are + /// [`crate::sm2_scalar::Sm2PublicScalar`] and whose points are a signer's public key and + /// the curve's own base point: every value it branches on is already known to an attacker. Do + /// not call this on anything derived from a private key or a per-message secret -- the + /// constant-time [`Self::add`] exists for that, and the scalar type split is what keeps the + /// two multipliers from being confused for one another. + pub(crate) fn add_vartime(&self, other: &Self) -> Self { + if self.is_infinity().to_bool() { + return *other; + } + if other.is_infinity().to_bool() { + return *self; + } + + let (generic, h, r) = self.generic_add(other); + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + generic + } + /// `self + (x2, y2)` for a **public**, affine `other` (`Z2 = 1`): [`Self::add_vartime`]'s + /// early-exit structure over [`Self::add_affine`]'s mixed formula, for the variable-time + /// multiplier's fixed side, whose precomputed multiples of `G` are stored affine. + /// `pub(crate)` for both of the reasons its two parents are. + pub(crate) fn add_vartime_affine(&self, x2: &Sm2FieldElement, y2: &Sm2FieldElement) -> Self { + if self.is_infinity().to_bool() { + return Self::from_affine(*x2, *y2); + } + + let z1_sq = self.z.mul(&self.z); + let u2 = x2.mul(&z1_sq); + let s2 = y2.mul(&self.z).mul(&z1_sq); + let h = self.x.sub(&u2); // U1 - U2, with U1 = X1 + let r = self.y.sub(&s2); // S1 - S2, with S1 = Y1 + if h.is_zero().to_bool() { + // Same affine x: either the same point (double it) or its negation (sum is infinity). + return if r.is_zero().to_bool() { self.double() } else { Self::INFINITY }; + } + + let h_squared = h.mul(&h); + let g = h_squared.mul(&h); // H^3 + let v = h_squared.mul(&self.x); // H^2 * U1 + let g_neg = g.negate(); + let acc = self.y.mul(&g_neg); // -S1 * H^3 + let g2 = g_neg.add(&v).add(&v); // 2V - H^3 + let x3 = r.mul(&r).sub(&g2); + let y3 = acc.add(&v.sub(&x3).mul(&r)); + let z3 = h.mul(&self.z); // H * Z1, with Z2 = 1 + Self { x: x3, y: y3, z: z3 } + } +} + +/// Selects `a` if `cond` is TRUE, else `b`, over every coordinate of a point. +fn select_point( + cond: Condition, + a: &Sm2JacobianPoint, + b: &Sm2JacobianPoint, +) -> Sm2JacobianPoint { + Sm2JacobianPoint { + x: Sm2FieldElement::from_internal_limbs(select_limbs( + cond, + &a.x.internal_limbs(), + &b.x.internal_limbs(), + )), + y: Sm2FieldElement::from_internal_limbs(select_limbs( + cond, + &a.y.internal_limbs(), + &b.y.internal_limbs(), + )), + z: Sm2FieldElement::from_internal_limbs(select_limbs( + cond, + &a.z.internal_limbs(), + &b.z.internal_limbs(), + )), + } +} + +fn select_limbs(cond: Condition, a: &[u64; 4], b: &[u64; 4]) -> [u64; 4] { + let mut out = [0u64; 4]; + ct::conditional_select(cond, a, b, &mut out); + out +} + +// `add_vartime` is `pub(crate)` -- deliberately unreachable from outside the crate, since calling +// it on a secret point would undo the constant-time discipline [`Sm2JacobianPoint::add`] exists for +// -- so no integration test can reach it. Its whole contract is that it computes the same group law +// as `add`, differing only in *how* it gets there, so that is what is pinned here: agreement on +// every exceptional case, including the ones a scalar multiplier over distinct precomputed +// multiples essentially never reaches by chance (a point added to itself, and a point added to its +// own negation). QUALITY_AND_STYLE's private-function carve-out applies. +#[cfg(test)] +mod tests { + use super::*; + use crate::sm2_domain::{G_X_LIMBS, G_Y_LIMBS}; + + #[test] + fn add_affine_agrees_with_add_on_every_case() { + let g = Sm2JacobianPoint::from_affine( + Sm2FieldElement::from_limbs(G_X_LIMBS), + Sm2FieldElement::from_limbs(G_Y_LIMBS), + ); + // A non-canonical-Z representation of G as well: `add_affine` reads `self.z`, so `self` + // must not be assumed affine even though `other` is. + let z = Sm2FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = Sm2JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + let selves = [g, g.double(), g.negate(), Sm2JacobianPoint::INFINITY, scaled_g]; + let others = [g, g.double(), g.negate()]; + for a in selves { + for b in others { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_affine(&bx, &by, Condition::::FALSE).to_affine(), + a.add(&b).to_affine(), + "add_affine disagrees with add" + ); + } + // The identity flag must win regardless of the coordinates passed alongside it. + assert_eq!( + a.add_affine(&g.x, &g.y, Condition::::TRUE).to_affine(), + a.to_affine(), + "add_affine with the identity flag must return self" + ); + } + } + + #[test] + fn add_vartime_affine_agrees_with_add_on_every_case() { + let g = Sm2JacobianPoint::from_affine( + Sm2FieldElement::from_limbs(G_X_LIMBS), + Sm2FieldElement::from_limbs(G_Y_LIMBS), + ); + let z = Sm2FieldElement::from_limbs(G_Y_LIMBS); + let z2 = z.mul(&z); + let scaled_g = Sm2JacobianPoint { x: g.x.mul(&z2), y: g.y.mul(&z2.mul(&z)), z }; + for a in [g, g.double(), g.negate(), Sm2JacobianPoint::INFINITY, scaled_g] { + for b in [g, g.double(), g.negate()] { + let (bx, by) = b.to_affine().unwrap(); + assert_eq!( + a.add_vartime_affine(&bx, &by).to_affine(), + a.add(&b).to_affine(), + "add_vartime_affine disagrees with add" + ); + } + } + } + + #[test] + fn add_vartime_agrees_with_add_on_every_case() { + let g = Sm2JacobianPoint::from_affine( + Sm2FieldElement::from_limbs(G_X_LIMBS), + Sm2FieldElement::from_limbs(G_Y_LIMBS), + ); + // G, 2G, -G and the identity cover all four of `add`'s exceptional cases pairwise, plus + // the ordinary one (e.g. G + 2G). + let points = [g, g.double(), g.negate(), Sm2JacobianPoint::INFINITY]; + for a in points { + for b in points { + assert_eq!( + a.add_vartime(&b).to_affine(), + a.add(&b).to_affine(), + "add_vartime disagrees with add" + ); + } + } + } +} diff --git a/crypto/ec/src/sm2_scalar.rs b/crypto/ec/src/sm2_scalar.rs new file mode 100644 index 000000000..00a8c73d3 --- /dev/null +++ b/crypto/ec/src/sm2_scalar.rs @@ -0,0 +1,441 @@ +//! The SM2 scalar field, arithmetic mod the curve order `n` (`draft-shen-sm2-ecdsa-02` Appendix +//! D), for SM2 signing's `r = e + x1` and `s = (1 + dA)^-1 (k - r dA) mod n` (GB/T 32918.2-2016 +//! §6.1 steps A5-A6, with `(1 + dA)^-1` inverted once per key) and verification's `t = r' + s'` +//! (§7.1 step B5). SM2 has no `k^-1`. +//! +//! Unlike `p`, `n` is not a Solinas prime -- there is no special shape to exploit for reduction -- +//! so this uses Montgomery arithmetic instead: [`Sm2ScalarField`] holds its value as `x*R mod n` +//! internally (`R = 2^256`), which turns multiplication mod `n` into one wide multiply plus one +//! *fixed-length* reduction pass (REDC) with no data-dependent trial subtraction. Addition and +//! subtraction don't need REDC at all -- Montgomery form is linear, so `(a*R mod n) + (b*R mod n) +//! mod n == (a+b)*R mod n` -- and reuse the same "modulus close to `2^256`" trick +//! [`crate::sm2::Sm2FieldElement`] uses for `p` (this one isn't Solinas-specific either: it only +//! needs `n < 2^256`, true here just as for P-256's own scalar field). +//! +//! # REDC +//! +//! [`redc`] is the textbook "Separated Operand Scanning" (SOS) Montgomery reduction (Koç, Acar & +//! Kaliski, *Analyzing and Comparing Montgomery Multiplication Algorithms*, 1996, Algorithm 2): for +//! `i` in `0..4`, compute `m = T[i] * n' mod 2^64` (`n' = -n⁻¹ mod 2^64`) and add `m*n` into `T` +//! at limb offset `i`, which zeroes `T[i]` by construction; after all 4 rounds `T`'s low 4 limbs +//! are zero and the result is `T`'s remaining limbs, reduced by at most one final subtraction of +//! `n`. Verified (not checked in) against 30,000 random trials plus the `(n-1)*(n-1)` worst case: +//! the reduction never touches beyond a 9-limb accumulator, which is what [`redc`] uses. + +use crate::nat; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +/// `n`, the order of the SM2 base point `G`, little-endian `u64` limbs (`draft-shen-sm2-ecdsa-02` +/// Appendix D). +pub const N_LIMBS: [u64; 4] = + [0x53bbf40939d54123, 0x7203df6b21c6052b, 0xffffffffffffffff, 0xfffffffeffffffff]; + +/// `2^256 mod n` -- both the correction constant [`Sm2ScalarField::add`] uses (mirroring +/// [`crate::sm2::Sm2FieldElement::add`]) and, not by coincidence, the Montgomery representation +/// of `1` (`R mod n` where `R = 2^256`), since `2^256 mod n` *is* `R mod n`. +const R_LIMBS: [u64; 4] = + [0xac440bf6c62abedd, 0x8dfc2094de39fad4, 0x0000000000000000, 0x0000000100000000]; + +/// `R^2 mod n`, used to bring a plain value into Montgomery form: `REDC(x * R^2) = x*R mod n`. +const R_SQUARED_LIMBS: [u64; 4] = + [0x901192af7c114f20, 0x3464504ade6fa2fa, 0x620fc84c3affe0d4, 0x1eb5e412a22b3d3b]; + +/// `-n^-1 mod 2^64`, the Montgomery reduction constant. +const N_PRIME: u64 = 0x327f9e8872350975; + +/// `n - 2`, little-endian `u64` limbs -- the public exponent [`Sm2ScalarField::invert`] raises +/// its base to, per Fermat's little theorem (`n` is prime). +const N_MINUS_2_LIMBS: [u64; 4] = + [0x53bbf40939d54121, 0x7203df6b21c6052b, 0xffffffffffffffff, 0xfffffffeffffffff]; + +/// An element of the SM2 scalar field, i.e. an integer mod `n`. Stored internally in Montgomery +/// form; see the module docs. +#[derive(Clone, Copy, Debug)] +pub struct Sm2ScalarField([u64; 4]); + +impl Sm2ScalarField { + /// The additive identity. `0`'s Montgomery form is `0` itself (`0 * R mod n == 0`). + pub const ZERO: Self = Self([0, 0, 0, 0]); + + /// The multiplicative identity. Its Montgomery form is `R mod n`, i.e. [`R_LIMBS`], not the + /// literal integer `1`. + pub const ONE: Self = Self(R_LIMBS); + + /// Builds a scalar-field element from little-endian `u64` limbs (an ordinary, non-Montgomery + /// value), reducing once if `limbs >= n` (the same single-conditional-subtraction argument as + /// [`crate::sm2::Sm2FieldElement::from_limbs`] applies: `limbs < 2^256 < 2n`), then + /// converting to Montgomery form. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + Self(redc(&widening_mul(&reduced, &R_SQUARED_LIMBS))) + } + + /// Returns the canonical little-endian `u64` limbs (an ordinary, non-Montgomery value), `< n`. + pub fn to_limbs(&self) -> [u64; 4] { + let mut wide = [0u64; 8]; + wide[..4].copy_from_slice(&self.0); + redc(&wide) + } + + /// TRUE iff this element is the additive identity. + pub fn is_zero(&self) -> Condition { + nat::is_zero(&self.0) + } + + /// `self + other mod n`. Operates directly on the Montgomery-form limbs: no REDC needed, since + /// Montgomery form is linear (see the module docs). + pub fn add(&self, other: &Self) -> Self { + let (sum, carry) = nat::add(&self.0, &other.0); + let (sum_plus_r, _) = nat::add(&sum, &R_LIMBS); + let (diff, borrow) = nat::sub(&sum, &N_LIMBS); + let mut when_no_carry = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &sum, &diff, &mut when_no_carry); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(carry), + &sum_plus_r, + &when_no_carry, + &mut result, + ); + Self(result) + } + + /// `self - other mod n`. + pub fn sub(&self, other: &Self) -> Self { + let (diff, borrow) = nat::sub(&self.0, &other.0); + let (corrected, _) = nat::add(&diff, &N_LIMBS); + let mut result = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &corrected, &diff, &mut result); + Self(result) + } + + /// `-self mod n`. + pub fn negate(&self) -> Self { + let (diff, _) = nat::sub(&N_LIMBS, &self.0); + let mut result = [0u64; 4]; + ct::conditional_select(self.is_zero(), &Self::ZERO.0, &diff, &mut result); + Self(result) + } + + /// `self * other mod n`, via Montgomery multiplication (widening multiply + [`redc`]). + pub fn mul(&self, other: &Self) -> Self { + Self(redc(&widening_mul(&self.0, &other.0))) + } + + /// Overwrites this value with zero, through a write the compiler may not elide. + /// + /// For the signing path, where a value of this type holds `d`, `k` or `(1 + d)^-1` and would + /// otherwise stay legible on the stack after the signature is returned. This type is `Copy` + /// and carries public values too (verification's `e`, `r'`, `s'`, `t`), so it is not + /// wrapped in [`bouncycastle_utils::secret::Secret`] -- that would cost its `Copy`, its `const + /// ZERO`/`ONE`, and a scrub on every intermediate including the public ones. The trade is that + /// scrubbing is the caller's job, on every path out of the function; see + /// `bouncycastle_sm2`'s `sign_with_k` for the intended use. + /// + /// This does not reach values the compiler kept in registers or spilled itself, and is + /// defence in depth rather than a guarantee. + pub fn zeroize(&mut self) { + bouncycastle_utils::secret::zeroize_in_place(&mut self.0); + } + + /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see + /// [`widening_square`]. + /// + /// Used only by [`Self::invert`]'s exponentiation loop, which calls it hundreds of times in + /// a row with nothing else competing for registers. (The point arithmetic is on the base + /// field, whose own `square` is likewise kept out of it; see + /// [`crate::p256::P256FieldElement::square`] for the measurements behind that.) + pub fn square(&self) -> Self { + Self(redc(&widening_square(&self.0))) + } + + /// Converts a secret scalar (`d` or `k`) into Montgomery form for field arithmetic (`r*d`, + /// `k - r*d`, ...). Reads the secret's limbs once into a plain value, the same "protects storage, not + /// every derived computation" boundary [`crate::sm2_comb::comb_multiply_base_point`] already + /// crosses to read `k`'s bits. + pub fn from_secret(secret: &Sm2Scalar) -> Self { + Self::from_limbs(*secret.limbs()) + } + + /// `self^-1 mod n`, or `0` if `self` is `0`. Fermat's little theorem (`n` is prime) over the + /// public exponent `n-2`, by the same fixed 4-bit-window exponentiation as + /// [`crate::p256::P256FieldElement::invert`] -- see its docs for the constant-time argument. + pub fn invert(&self) -> Self { + // self^0 .. self^15, indexed by nibble value. + let mut table = [Self::ONE; 16]; + table[1] = *self; + for i in 2..16 { + table[i] = table[i - 1].mul(self); + } + + let mut result = Self::ONE; + for limb_idx in (0..4).rev() { + let limb = N_MINUS_2_LIMBS[limb_idx]; + for nibble_idx in (0..16).rev() { + for _ in 0..4 { + result = result.square(); + } + // `limb` is one word of a compile-time constant exponent and `nibble_idx` a loop + // index, so both the index into `table` and this branch depend on public data + // only: the sequence of operations is fixed at compile time and identical on every + // call. + let nibble = ((limb >> (4 * nibble_idx)) & 0xf) as usize; + if nibble != 0 { + result = result.mul(&table[nibble]); + } + } + } + result + } +} + +impl PartialEq for Sm2ScalarField { + /// Constant-time: every limb is compared regardless of where (or whether) a difference is + /// found. Comparing the Montgomery-form limbs directly is valid: the map `x -> x*R mod n` is a + /// bijection, so two elements are equal iff their Montgomery forms are. + fn eq(&self, other: &Self) -> bool { + let mut acc = Condition::::TRUE; + for i in 0..4 { + acc &= Condition::::is_equal(self.0[i], other.0[i]); + } + acc.to_bool() + } +} + +impl Eq for Sm2ScalarField {} + +/// Schoolbook widening multiply, identical in shape to +/// [`crate::sm2`]'s private helper of the same name; see that module's docs for why this isn't +/// expressed generically over `Nat`. +fn widening_mul(a: &[u64; 4], b: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + for i in 0..4 { + let mut carry: u128 = 0; + for j in 0..4 { + let idx = i + j; + let prod = (a[i] as u128) * (b[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + let mut k = i + 4; + while k < 8 { + let s = (result[k] as u128) + carry; + result[k] = s as u64; + carry = s >> 64; + k += 1; + } + } + result +} + +/// Schoolbook squaring of a 4-limb value into its 8-limb square. +/// +/// `a * a` is symmetric: the product `a_i * a_j` appears twice for every `i != j`. Forming each of +/// those once and doubling costs `L(L+1)/2` limb multiplications -- 10 at this width against +/// the 16 [`widening_mul`] would form for the same value. +/// +/// Three passes: the off-diagonal products `a_i * a_j` for `i < j`; a doubling of the whole +/// accumulator; then the diagonal squares `a_i * a_i` added in at limb `2i`. Writing `result[i + +/// 4]` in the first pass is an assignment rather than an accumulation because row `i` only ever +/// reaches limbs `2i + 1 ..= i + 4 - 1`, and no earlier row reaches limb `i + 4` either, so that +/// limb is still zero when the row's final carry lands on it. The doubling cannot overflow: the +/// off-diagonal sum is strictly less than `a^2 / 2`. +/// +/// Verified against Python's arbitrary-precision `**2` over 20,000 random 4-limb values plus the +/// all-ones worst case before being written here. +fn widening_square(a: &[u64; 4]) -> [u64; 8] { + let mut result = [0u64; 8]; + + // Off-diagonal products, each formed once. + for i in 0..4 { + let mut carry: u128 = 0; + for j in (i + 1)..4 { + let idx = i + j; + let prod = (a[i] as u128) * (a[j] as u128) + (result[idx] as u128) + carry; + result[idx] = prod as u64; + carry = prod >> 64; + } + result[i + 4] = carry as u64; + } + + // Every off-diagonal product appears twice in the square. + let mut carry = 0u64; + for limb in result.iter_mut() { + let next_carry = *limb >> 63; + // `*limb << 1` always leaves bit 0 clear and `carry` is only ever 0 or 1, so the two + // operands are disjoint: mutating this `|` to `^` is an accepted equivalent, not a bug. + *limb = (*limb << 1) | carry; + carry = next_carry; + } + debug_assert_eq!(carry, 0, "doubling the off-diagonal sum overflowed 8 limbs"); + + // Diagonal squares. + let mut carry: u128 = 0; + for i in 0..4 { + let square = (a[i] as u128) * (a[i] as u128); + let low = (result[2 * i] as u128) + ((square as u64) as u128) + carry; + result[2 * i] = low as u64; + let high = (result[2 * i + 1] as u128) + (square >> 64) + (low >> 64); + result[2 * i + 1] = high as u64; + carry = high >> 64; + } + debug_assert_eq!(carry, 0, "adding the diagonal overflowed 8 limbs"); + + result +} + +/// Montgomery reduction (SOS method): given an 8-limb value `t < n*R`, returns `t * R^-1 mod n` as +/// 4 limbs. See the module docs for the algorithm. +/// +/// After the 4 reduction rounds, the low 4 limbs of the 9-limb accumulator are zero and the +/// result -- `t * R^-1`, an integer, not yet reduced mod `n` -- occupies the remaining 5 limbs +/// (`acc[4..9]`): unlike [`crate::sm2::reduce`], whose modulus is small enough that the analogous +/// step needs only one limb of headroom, `n` is close enough to `2^256` that `2n` can exceed it, +/// so a 9th limb (`acc[8]`, verified to always be `0` or `1`) is needed. The final step folds that +/// bit in exactly like [`crate::sm2::Sm2FieldElement::add`]'s carry-aware correction: if +/// `acc[8] == 1`, the true value is `high + 2^256`, `>= 2^256 > n`, so `high + (2^256 mod n)` (i.e. +/// `+ R_LIMBS`) is the reduced result; otherwise it's `high`, conditionally reduced once more if +/// `high >= n`. +fn redc(t: &[u64; 8]) -> [u64; 4] { + let mut acc = [0u64; 9]; + acc[..8].copy_from_slice(t); + + for i in 0..4 { + let m = acc[i].wrapping_mul(N_PRIME); + let mn = widening_mul(&[m, 0, 0, 0], &N_LIMBS); + debug_assert_eq!(mn[5], 0); + debug_assert_eq!(mn[6], 0); + debug_assert_eq!(mn[7], 0); + + let mut carry: u128 = 0; + for j in 0..5 { + let idx = i + j; + let s = (acc[idx] as u128) + (mn[j] as u128) + carry; + acc[idx] = s as u64; + carry = s >> 64; + } + // Propagate the carry through every limb above the window, up to the accumulator's + // proven 9-limb bound. The trip count depends only on the public round index `i`, + // never on the operands: once the carry has propagated out, the remaining iterations + // add 0. A `while carry != 0` here would be a secret-dependent branch on the signing + // path. + for idx in (i + 5)..9 { + let s = (acc[idx] as u128) + carry; + acc[idx] = s as u64; + carry = s >> 64; + } + debug_assert_eq!(carry, 0, "carry must be fully propagated within the 9-limb bound"); + } + debug_assert_eq!(&acc[..4], &[0u64; 4], "REDC's low limbs must be cleared after 4 rounds"); + debug_assert!(acc[8] == 0 || acc[8] == 1, "REDC's top limb must be a single bit"); + + let high: [u64; 4] = [acc[4], acc[5], acc[6], acc[7]]; + let (high_plus_r, _) = nat::add(&high, &R_LIMBS); // used when acc[8] == 1 + let (diff, borrow) = nat::sub(&high, &N_LIMBS); // used when acc[8] == 0: is high >= n? + let mut when_no_extra_bit = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(borrow), + &high, + &diff, + &mut when_no_extra_bit, + ); + let mut result = [0u64; 4]; + ct::conditional_select( + Condition::::from_lsb(acc[8]), + &high_plus_r, + &when_no_extra_bit, + &mut result, + ); + result +} + +fn reduce_once(limbs: [u64; 4]) -> [u64; 4] { + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut result = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut result); + result +} + +/// A scalar mod `n`, `< n`, held in `Secret` -- for a value that must never be handled in +/// non-constant time: a private key `d`, or the per-signature secret `k`. There is deliberately no +/// conversion from `Sm2Scalar` to [`Sm2PublicScalar`]: the crate's constant-time and +/// variable-time scalar multipliers take `&Sm2Scalar` and `&Sm2PublicScalar` respectively, so a +/// caller cannot pass a secret scalar to the variable-time multiplier by accident -- the type +/// system forbids it, rather than relying on a reviewer to notice. `Clone`/`Debug`/`PartialEq`/`Eq` +/// forward to [`Secret`]'s own impls (`Debug` redacting, +/// `PartialEq` constant-time), which is what `core::traits::SignaturePrivateKey`'s bound needs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Sm2Scalar(Secret<[u64; 4]>); + +impl Sm2Scalar { + /// Builds a secret scalar from little-endian `u64` limbs, reducing once if `limbs >= n` (see + /// [`Sm2ScalarField::from_limbs`]'s doc for why one conditional subtraction always suffices). + pub fn from_limbs(limbs: [u64; 4]) -> Self { + let mut secret = Secret::<[u64; 4]>::new(); + *secret = reduce_once(limbs); + Self(secret) + } + + /// Builds a secret scalar from SEC 1 §2.3.7 big-endian octets, reducing once if `>= n`. + pub fn from_be_bytes(bytes: &[u8; 32]) -> Self { + Self::from_limbs(crate::sm2_sec1::limbs_from_be_bytes(bytes)) + } + + /// Encodes to SEC 1 §2.3.7 big-endian octets -- the wire form of an SM2 private key. + /// Momentarily holds the plain value in the returned array, same as any other `encode()` on a + /// `SignaturePrivateKey`: the caller asked for the bytes. + pub fn to_be_bytes(&self) -> [u8; 32] { + crate::sm2_sec1::be_bytes_from_limbs(&self.0) + } + + /// The scalar's limbs, for the crate's own multiplier and field-arithmetic implementations to + /// read (see [`Sm2ScalarField::from_secret`]). Not exposed outside the crate: nothing outside + /// `bouncycastle-ec` should ever hold a bare, unprotected copy of a secret scalar's value. + pub(crate) fn limbs(&self) -> &[u64; 4] { + &self.0 + } +} + +/// A scalar mod `n`, `< n`, held as a plain (non-secret) value -- for a value that is public by +/// construction: the `s'` and `t = r' + s'` scalars SM2 verification computes from a signature +/// and public data, or a message-derived value. See [`Sm2Scalar`]'s docs for why there is no +/// conversion the other way. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Sm2PublicScalar([u64; 4]); + +impl Sm2PublicScalar { + /// Builds a public scalar from little-endian `u64` limbs, reducing once if `limbs >= n`. + pub fn from_limbs(limbs: [u64; 4]) -> Self { + Self(reduce_once(limbs)) + } + + /// Returns the scalar's little-endian `u64` limbs, `< n`. + pub fn to_limbs(&self) -> [u64; 4] { + self.0 + } +} + +#[cfg(test)] +mod tests { + // `Sm2Scalar::limbs` is `pub(crate)`, deliberately not reachable from an integration test + // (nothing outside the crate should get a bare copy of a secret scalar) -- exercised here + // instead, per QUALITY_AND_STYLE's carve-out for code unreachable through the public API. + use super::*; + + #[test] + fn secret_scalar_reduces_and_round_trips() { + let limbs: [u64; 4] = [1, 2, 3, 4]; + let scalar = Sm2Scalar::from_limbs(limbs); + assert_eq!(*scalar.limbs(), limbs); + } + + #[test] + fn secret_scalar_reduces_out_of_range_input() { + let scalar = Sm2Scalar::from_limbs(N_LIMBS); + assert_eq!(*scalar.limbs(), [0, 0, 0, 0]); + } +} diff --git a/crypto/ec/src/sm2_sec1.rs b/crypto/ec/src/sm2_sec1.rs new file mode 100644 index 000000000..b50e85507 --- /dev/null +++ b/crypto/ec/src/sm2_sec1.rs @@ -0,0 +1,228 @@ +//! SEC 1 v2.0 (Certicom, May 2009) §2.3.3/§2.3.4 point encoding and decoding for SM2. Identical in +//! shape to [`crate::p256_sec1`] -- see that module's docs for the full reasoning -- with SM2's +//! own field type and domain constants substituted. `draft-shen-sm2-ecdsa-02` itself does not +//! specify a byte-level point encoding (it defines the "point compression" notation `y~P` but not +//! the wire format), so this reuses SEC 1's near-universal encoding, the same convention every +//! other curve in this crate follows and the one real-world SM2 implementations interoperate with. +//! +//! # Encodings +//! +//! Field elements are encoded big-endian, fixed-width (SEC 1 §2.3.5/§2.3.7 for `GF(p)`) -- the +//! opposite byte order from this crate's internal little-endian `u64` limbs, so +//! [`limbs_from_be_bytes`] and [`be_bytes_from_limbs`] convert at the boundary. +//! +//! [`encode_uncompressed`] is SEC 1 §2.3.3 case 3 (`04 || X || Y`) and [`encode_compressed`] is +//! case 2 (`02 || X` or `03 || X`, tag selected by `y mod 2`). Both take affine coordinates +//! directly rather than a [`Sm2JacobianPoint`], not a `[u8; 1]` "point at infinity" case: SEC 1 +//! §2.3.3 case 1 encodes `∅` as a single `0x00` octet, but an SM2 public key -- the only +//! caller -- can never legitimately be `∅`, so pushing the infinity check to the type (a caller +//! must have already gotten `Some(x, y)` out of [`Sm2JacobianPoint::to_affine`]) makes encoding +//! infinity a compile error instead of a runtime one. +//! +//! [`decode`] is SEC 1 §2.3.4 cases 2 and 3 (again, no case 1, for the same reason). It performs +//! partial public-key validation as part of decoding, not as a separate step: not-infinity holds +//! by construction (see above), `x`, `y` in `[0, p)` is [`limbs_less_than_p`], and on-the-curve is +//! explicit for the uncompressed case and automatic for the compressed case, since its `y` is +//! constructed as an actual square root of `x^3 - 3x + b`. The additional "full validation" step, +//! `nQ = ∅`, is not needed: this curve has cofactor `h = 1` (derived directly from the Hasse bound +//! using `p` and `n` from `draft-shen-sm2-ecdsa-02` Appendix D -- `|n - (p+1)| <= 2*sqrt(p)` holds +//! only for `h = 1`, verified in Python, not assumed by analogy to any other curve), so a point +//! that passes partial validation has order dividing the prime `n` and is not the identity, hence +//! has order exactly `n` -- `nQ = ∅` is implied, not a separate check. +//! +//! # Compressed decoding +//! +//! `p ≡ 3 (mod 4)` (verified directly in this module's tests), so a square root of a residue `α` +//! is `β = α^((p+1)/4) mod p` (`β² = α^((p+1)/2) = α^((p-1)/2) * α = α` when `α` is a residue, by +//! Euler's criterion). [`SQRT_EXPONENT`] is `(p+1)/4`. [`decode`] verifies `β² == α` and rejects if +//! not: that check is what makes "α has no square root" (SEC 1 §2.3.4 step 2.4.1) detectable, since +//! the exponentiation itself is total and returns *something* whether or not `α` is a residue. +//! +//! Verified (not checked in) before any of this was written: the decompression formula against +//! 300 random points' known `y` (recomputed from `x` and `y`'s parity bit), and that a non-residue +//! `x` is correctly rejected. + +use crate::nat; +use crate::sm2::{P_LIMBS, Sm2FieldElement}; +use crate::sm2_domain::B_LIMBS; +use crate::sm2_point::Sm2JacobianPoint; + +/// `(p + 1) / 4`, the exponent [`decode`]'s compressed-point square root raises `α` to. +const SQRT_EXPONENT: [u64; 4] = + [0x4000000000000000, 0xffffffffc0000000, 0xffffffffffffffff, 0x3fffffffbfffffff]; + +/// Converts 32 big-endian bytes to little-endian `u64` limbs. +pub fn limbs_from_be_bytes(bytes: &[u8; 32]) -> [u64; 4] { + [ + u64::from_be_bytes(bytes[24..32].try_into().unwrap()), + u64::from_be_bytes(bytes[16..24].try_into().unwrap()), + u64::from_be_bytes(bytes[8..16].try_into().unwrap()), + u64::from_be_bytes(bytes[0..8].try_into().unwrap()), + ] +} + +/// Converts little-endian `u64` limbs to 32 big-endian bytes. +pub fn be_bytes_from_limbs(limbs: &[u64; 4]) -> [u8; 32] { + let mut out = [0u8; 32]; + out[0..8].copy_from_slice(&limbs[3].to_be_bytes()); + out[8..16].copy_from_slice(&limbs[2].to_be_bytes()); + out[16..24].copy_from_slice(&limbs[1].to_be_bytes()); + out[24..32].copy_from_slice(&limbs[0].to_be_bytes()); + out +} + +fn limbs_less_than_p(limbs: &[u64; 4]) -> bool { + let (_, borrow) = nat::sub(limbs, &P_LIMBS); + borrow == 1 +} + +/// `true` iff either coordinate is outside `[0, p)` (SP 800-186 Appendix D.1.1.1 step 2). +fn either_out_of_range(x_limbs: &[u64; 4], y_limbs: &[u64; 4]) -> bool { + !limbs_less_than_p(x_limbs) || !limbs_less_than_p(y_limbs) +} + +/// `y^2 == x^3 - 3x + b`. +fn is_on_curve(x: &Sm2FieldElement, y: &Sm2FieldElement) -> bool { + let lhs = y.mul(y); + let x_cubed = x.mul(x).mul(x); + let three_x = x.add(x).add(x); + let rhs = x_cubed.sub(&three_x).add(&Sm2FieldElement::from_limbs(B_LIMBS)); + lhs == rhs +} + +/// `base^SQRT_EXPONENT mod p`. Plain (not masked) square-and-multiply: `decode`'s only caller, +/// unlike everywhere else in this crate, only ever runs on public data (an incoming encoded +/// point), so there is nothing to protect here. +fn pow_public(base: &Sm2FieldElement, exponent_limbs: &[u64; 4]) -> Sm2FieldElement { + let mut result = Sm2FieldElement::ONE; + for limb_idx in (0..4).rev() { + let limb = exponent_limbs[limb_idx]; + for bit in (0..64).rev() { + result = result.square(); + if (limb >> bit) & 1 == 1 { + result = result.mul(base); + } + } + } + result +} + +/// SEC 1 §2.3.3 case 3: `04 || X || Y`, 65 bytes. +pub fn encode_uncompressed(x: &Sm2FieldElement, y: &Sm2FieldElement) -> [u8; 65] { + let mut out = [0u8; 65]; + out[0] = 0x04; + out[1..33].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out[33..65].copy_from_slice(&be_bytes_from_limbs(&y.to_limbs())); + out +} + +/// SEC 1 §2.3.3 case 2: `02 || X` or `03 || X`, 33 bytes, tag selected by `y mod 2`. +pub fn encode_compressed(x: &Sm2FieldElement, y: &Sm2FieldElement) -> [u8; 33] { + let mut out = [0u8; 33]; + out[0] = if y.to_limbs()[0] & 1 == 0 { 0x02 } else { 0x03 }; + out[1..33].copy_from_slice(&be_bytes_from_limbs(&x.to_limbs())); + out +} + +/// Decodes and validates a SEC 1 §2.3.4 point encoding: 33 bytes (compressed, tag `0x02`/`0x03`) +/// or 65 bytes (uncompressed, tag `0x04`). See the module docs for exactly which SP 800-186 +/// Appendix D.1.1.1 checks this performs and why it constitutes full (not merely partial) +/// validation for SM2. Returns `None` for anything invalid. +pub fn decode(bytes: &[u8]) -> Option<(Sm2FieldElement, Sm2FieldElement)> { + match bytes.len() { + 33 => { + let y_tilde: u64 = match bytes[0] { + 0x02 => 0, + 0x03 => 1, + _ => return None, + }; + let x_bytes: [u8; 32] = bytes[1..33].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + if !limbs_less_than_p(&x_limbs) { + return None; + } + let x = Sm2FieldElement::from_limbs(x_limbs); + let x_cubed = x.mul(&x).mul(&x); + let three_x = x.add(&x).add(&x); + let alpha = x_cubed.sub(&three_x).add(&Sm2FieldElement::from_limbs(B_LIMBS)); + let beta = pow_public(&alpha, &SQRT_EXPONENT); + if beta.mul(&beta) != alpha { + return None; + } + let beta_parity = beta.to_limbs()[0] & 1; + let y = if beta_parity == y_tilde { beta } else { beta.negate() }; + Some((x, y)) + } + 65 => { + if bytes[0] != 0x04 { + return None; + } + let x_bytes: [u8; 32] = bytes[1..33].try_into().ok()?; + let y_bytes: [u8; 32] = bytes[33..65].try_into().ok()?; + let x_limbs = limbs_from_be_bytes(&x_bytes); + let y_limbs = limbs_from_be_bytes(&y_bytes); + if either_out_of_range(&x_limbs, &y_limbs) { + return None; + } + let x = Sm2FieldElement::from_limbs(x_limbs); + let y = Sm2FieldElement::from_limbs(y_limbs); + if !is_on_curve(&x, &y) { + return None; + } + Some((x, y)) + } + _ => None, + } +} + +/// Decodes and validates an encoded public key (see [`decode`]) directly into a +/// [`Sm2JacobianPoint`]. +pub fn decode_point(bytes: &[u8]) -> Option { + let (x, y) = decode(bytes)?; + Some(Sm2JacobianPoint::from_affine(x, y)) +} + +// `limbs_less_than_p` and `either_out_of_range` are private and, through `decode`, only ever +// reached on inputs that also fail (or coincidentally would also fail) a later curve-membership +// check, so no integration test through the public API can pin either one's pass/fail boundary +// independent of that coincidence -- the QUALITY_AND_STYLE.md "private function, known-answer, +// can't be reached cleanly from outside the crate" exception applies to both. Known answers for +// `limbs_less_than_p`: GB/T 32918.5-2017 (draft Appendix D) gives `p`; 0 and `p - 1` are its immediate neighbours +// inside `[0, p)`, and `p` itself plus a value with only a non-adjacent limb bumped above `p`'s +// are its immediate neighbours outside it. `either_out_of_range` is exercised over all four +// in-range/out-of-range combinations of its two arguments, which is what pins OR against AND. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn p_is_3_mod_4() { + assert_eq!(P_LIMBS[0] & 0b11, 3); + } + + #[test] + fn limbs_less_than_p_boundary_cases() { + assert!(limbs_less_than_p(&[0, 0, 0, 0])); + + let p_minus_1 = [P_LIMBS[0] - 1, P_LIMBS[1], P_LIMBS[2], P_LIMBS[3]]; + assert!(limbs_less_than_p(&p_minus_1)); + + assert!(!limbs_less_than_p(&P_LIMBS)); + + let p_plus_2_pow_64 = [P_LIMBS[0], P_LIMBS[1] + 1, P_LIMBS[2], P_LIMBS[3]]; + assert!(!limbs_less_than_p(&p_plus_2_pow_64)); + + assert!(!limbs_less_than_p(&[u64::MAX; 4])); + } + + #[test] + fn either_out_of_range_requires_only_one_coordinate_to_be_out_of_range() { + let in_range = [0u64, 0, 0, 0]; + let out_of_range = P_LIMBS; + + assert!(!either_out_of_range(&in_range, &in_range)); + assert!(either_out_of_range(&out_of_range, &in_range)); + assert!(either_out_of_range(&in_range, &out_of_range)); + assert!(either_out_of_range(&out_of_range, &out_of_range)); + } +} diff --git a/crypto/ec/src/sm2_wnaf.rs b/crypto/ec/src/sm2_wnaf.rs new file mode 100644 index 000000000..fb4cb36f2 --- /dev/null +++ b/crypto/ec/src/sm2_wnaf.rs @@ -0,0 +1,175 @@ +//! Interleaved windowed-NAF (wNAF) Shamir's-trick multiplication: `[u]G + [v]Q`, for SM2 +//! verification's `(x1', y1') = [s']G + [t]PA` (GB/T 32918.2-2016 §7.1 step B6). Both scalars and +//! both points are public -- `G` is the curve's fixed base point and `Q` a signer's public key -- +//! so unlike +//! [`crate::sm2_comb`], this multiplier takes [`Sm2PublicScalar`], not [`crate::sm2_scalar::Sm2Scalar`], +//! and is written for speed, not constant time: it branches freely on the digits it computes, +//! and adds points with `add_vartime` rather than the constant-time `add` (see that method's +//! docs for why that is both sound here and worth roughly a third of every point addition). +//! +//! # wNAF +//! +//! A width-`w` NAF (non-adjacent form) represents a scalar as signed digits, each `0` or odd and +//! in `(-2^(w-1), 2^(w-1))`, with the property that any `w` consecutive digits contain at most one +//! nonzero one -- so on average only 1-in-(w+1) digits need a point addition. [`compute_wnaf`] +//! computes this the standard way: while the running value is nonzero, if it's odd, take +//! `digit = value mod 2^w`, recentre it into `(-2^(w-1), 2^(w-1)]` if it's `>= 2^(w-1)`, and +//! subtract it off (a small, `< 2^w`, signed adjustment -- a single-limb add or subtract of the +//! 4-limb value); then shift right by one bit regardless. Width `w = 5` is used here, giving 8 +//! precomputed odd multiples per point ([`ODD_MULTIPLE_COUNT`]). +//! +//! `G` is the curve's constant, so its odd multiples are a compile-time table of affine points +//! ([`G_ODD_MULTIPLES_X`]/[`G_ODD_MULTIPLES_Y`], width 7 -- [`G_WIDTH`]) and every `[u]G` digit +//! is a mixed addition; only `Q`'s table (width 5) is built per call. +//! +//! [`shamir_multiply`] runs both scalars' wNAF digit sequences through the same doubling loop +//! (Shamir's trick): one doubling per digit position serves both `[u]G` and `[v]Q` at once, adding +//! in whichever precomputed odd multiple (or its negation) each sequence's digit at that position +//! calls for. +//! +//! Verified (not checked in) two ways before any of this was written: the digit encoding, against +//! 2000 random `k` (does `sum(digit_i * 2^i) == k`, does every 5-digit window have at most one +//! nonzero entry, are all digits odd and correctly bounded); and the full multiplier, against the +//! standard affine group law (`a = -3`) computed independently via repeated doubling and +//! conditional addition -- including `u = 0`, `v = 0`, both `0`, and `u`/`v = n-1`. + +use crate::nat; +use crate::sm2::Sm2FieldElement; +use crate::sm2_point::Sm2JacobianPoint; +use crate::sm2_scalar::Sm2PublicScalar; +use crate::sm2_wnaf_table::{G_ODD_MULTIPLES_X, G_ODD_MULTIPLES_Y}; + +/// Window for `Q`, whose odd multiples are computed per call: 8 point additions to build the +/// table, one addition per ~6 digits to use it. +const WIDTH: usize = 5; +/// Window for `G`, whose odd multiples are a compile-time table +/// ([`G_ODD_MULTIPLES_X`]/[`G_ODD_MULTIPLES_Y`]): nothing to build, so the widest window a +/// signed `i8` digit allows, giving one addition per ~8 digits instead of ~6, and every one of +/// them a mixed addition against an affine entry. +const G_WIDTH: usize = 7; +const WNAF_LEN: usize = 257; +// Mutating this expression (e.g. `WIDTH - 2` -> `+ 2`) only grows the table `odd_multiples` builds +// and `lookup_signed` reads from; the actual digit range `compute_wnaf` can produce is bounded by +// `WIDTH` itself (unchanged), so the extra entries are simply never indexed -- an accepted mutant, +// the same one already documented in `crate::p256k1_wnaf` and `crate::bp256r1_wnaf`. +const ODD_MULTIPLE_COUNT: usize = 1 << (WIDTH - 2); + +/// `[u]G + [v]Q`. +pub fn shamir_multiply( + u: &Sm2PublicScalar, + v: &Sm2PublicScalar, + q: &Sm2JacobianPoint, +) -> Sm2JacobianPoint { + let du = compute_wnaf(u.to_limbs(), G_WIDTH); + let dv = compute_wnaf(v.to_limbs(), WIDTH); + + let table_q = odd_multiples(q); + + let mut r = Sm2JacobianPoint::INFINITY; + for i in (0..WNAF_LEN).rev() { + r = r.double(); + if let Some((x, y)) = lookup_signed_g(du[i]) { + r = r.add_vartime_affine(&x, &y); + } + if let Some(add_q) = lookup_signed(&table_q, dv[i]) { + r = r.add_vartime(&add_q); + } + } + r +} + +fn shr1(limbs: &[u64; 4]) -> [u64; 4] { + // Each `limbs[i] >> 1` only occupies bits 0-62 (its own bit 63 is shifted out), and + // `(limbs[i+1] & 1) << 63` only ever occupies bit 63 -- disjoint, so `|` and `^` agree here, + // the same equivalence already documented in `crate::bp256r1_wnaf::shr1`. + [ + (limbs[0] >> 1) | ((limbs[1] & 1) << 63), + (limbs[1] >> 1) | ((limbs[2] & 1) << 63), + (limbs[2] >> 1) | ((limbs[3] & 1) << 63), + limbs[3] >> 1, + ] +} + +/// The width-`width` wNAF digits of `k` (`width` at most 7, so a digit fits an `i8`), LSB-first; unused trailing entries are `0`. Not +/// constant time: only ever called on public scalars (see the module docs). +fn compute_wnaf(k_limbs: [u64; 4], width: usize) -> [i8; WNAF_LEN] { + let mut digits = [0i8; WNAF_LEN]; + let mut k = k_limbs; + let mut pos = 0; + while k != [0, 0, 0, 0] { + if k[0] & 1 == 1 { + // Mutating this mask (e.g. `(1 << WIDTH) - 1` -> `+ 1`) still produces a correct + // digit sequence: verified in Python over 20,000 random 256-bit k that a mask + // narrower than WIDTH bits (here, one that keeps only bits 0 and 5) still yields + // `sum(digit_i * 2^i) == k`, because every digit this narrower mask can ever produce + // (`{-1, 0, 1}` for this particular mutated mask) still gets subtracted off correctly + // before the next right-shift -- the loop just falls back to a slower, non-windowed + // (one-bit-per-step) encoding rather than width-5 wNAF, which is still within + // WNAF_LEN's bound and mathematically equivalent, just without the windowing speedup. + let mut digit = (k[0] & ((1 << width) - 1)) as i16; + if digit >= 1 << (width - 1) { + digit -= 1 << width; + } + k = if digit >= 0 { + let (new_k, borrow) = nat::sub(&k, &[digit as u64, 0, 0, 0]); + debug_assert_eq!(borrow, 0); + new_k + } else { + let (new_k, carry) = nat::add(&k, &[(-digit) as u64, 0, 0, 0]); + debug_assert_eq!(carry, 0); + new_k + }; + debug_assert!(pos < WNAF_LEN, "wNAF encoding overflowed its verified bound"); + digits[pos] = digit as i8; + } + k = shr1(&k); + pos += 1; + } + digits +} + +/// The odd multiples `1*p, 3*p, 5*p, ..., (2*ODD_MULTIPLE_COUNT - 1)*p`, indexed so entry `i` +/// holds `(2i+1)*p`. +fn odd_multiples(p: &Sm2JacobianPoint) -> [Sm2JacobianPoint; ODD_MULTIPLE_COUNT] { + let double_p = p.double(); + let mut table = [*p; ODD_MULTIPLE_COUNT]; + let mut cur = *p; + for entry in table.iter_mut() { + *entry = cur; + cur = cur.add_vartime(&double_p); + } + table +} + +/// `digit * p` where `p` is the point `table` was built from ([`odd_multiples`]), or `None` for +/// digit `0`. +fn lookup_signed( + table: &[Sm2JacobianPoint; ODD_MULTIPLE_COUNT], + digit: i8, +) -> Option { + // Every accepted-mutant survivor below relies on `digit` always being odd (it's built from an + // odd `k[0]` in `compute_wnaf`, see that function's docs): `digit > 0` vs `>= 0` agree once + // the `digit == 0` case is already handled above, and for odd `digit`, `(digit - 1) / 2 == + // digit / 2` (and symmetrically for `-digit`), since integer division already floors an odd + // numerator down to the same value subtracting 1 first would -- the same equivalence already + // documented in `crate::bp256r1_wnaf::lookup_signed`. + if digit == 0 { + None + } else if digit > 0 { + Some(table[(digit as usize - 1) / 2]) + } else { + Some(table[(-digit as usize - 1) / 2].negate()) + } +} + +/// `digit * G` for a width-[`G_WIDTH`] digit, read from the compile-time table of odd multiples +/// of `G` as affine coordinates (negating `y` for a negative digit), or `None` for digit `0`. +fn lookup_signed_g(digit: i8) -> Option<(Sm2FieldElement, Sm2FieldElement)> { + if digit == 0 { + return None; + } + let index = (digit.unsigned_abs() as usize - 1) / 2; + let x = Sm2FieldElement::from_limbs(G_ODD_MULTIPLES_X[index]); + let y = Sm2FieldElement::from_limbs(G_ODD_MULTIPLES_Y[index]); + Some(if digit > 0 { (x, y) } else { (x, y.negate()) }) +} diff --git a/crypto/ec/src/sm2_wnaf_table.rs b/crypto/ec/src/sm2_wnaf_table.rs new file mode 100644 index 000000000..56af2f8ec --- /dev/null +++ b/crypto/ec/src/sm2_wnaf_table.rs @@ -0,0 +1,105 @@ +//! The checked-in odd multiples of the base point `G` for [`crate::sm2_wnaf`]'s fixed side: +//! `G_ODD_MULTIPLES_X[i]`/`G_ODD_MULTIPLES_Y[i]` are the affine coordinates of `(2i + 1) G`, for +//! `i` in `0..32`, as little-endian `u64` limbs -- the table a width-7 wNAF over `G` indexes. +//! `Q`'s table is built at run time because `Q` is the signer's key; `G` is the curve's constant, +//! so its table need not be. +//! +//! Generated in Python from the affine group law (SP 800-186 Appendix A.1.1) and draft-shen-sm2-ecdsa-02 Appendix D's `G`, +//! independently of this crate's arithmetic, and checked there by `n * G == infinity`; this +//! module's own test regenerates every entry with the crate's point arithmetic and compares. + +/// `x` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_X: [[u64; 4]; 32] = [ + [0x715a4589334c74c7, 0x8fe30bbff2660be1, 0x5f9904466a39c994, 0x32c4ae2c1f198119], + [0xe26918f1d0509ebf, 0xa13f6bd945302244, 0xbe2daa8cdb41e24c, 0xa97f7cd4b3c993b4], + [0xa575da57cc372a9e, 0x344a417b7fce19db, 0x040e008fdd5eb77a, 0xc749061668652e26], + [0xcd27e384d9fcaf15, 0xa80198337744ee78, 0xfdbe86a75c139906, 0xddf092555409c19d], + [0x173472a58cca247e, 0xfe8d59cb43619e4f, 0x0b4a2444a46a74c5, 0xa27233f3a5959508], + [0x668c74d78ce20ace, 0x125dcdd589c2ff82, 0x7c1aab770f67f543, 0x04b3cb10c9c6d8e2], + [0x2b834f1d509b9cd0, 0x7bea65c6421e189e, 0xfa804513275f58aa, 0x952072d6ff9c65bd], + [0x461b1bbcb80f829c, 0x3b424f35f0ecce4c, 0x3291676c38d39324, 0xf73b839f13912c1a], + [0x36969f8b77125e6a, 0xbf5113ab85b5ed44, 0x1c993f01115c57f7, 0xdd18aa4aec26eac4], + [0x1323f252e493952a, 0xf7c96c55940a4726, 0xe2e0586bb5dc9419, 0xa68b2ec49d1921d0], + [0xe1de5f100950c8ab, 0x5b551c86a8976933, 0x137cbf4e8aa50535, 0x6407be639bc5b738], + [0x4dbb2fcd069d45db, 0xbb570a9f430ea7f6, 0x15c07a8c04226fe3, 0xac8df677299e8288], + [0x0263b20d539e4139, 0x7e8581fbbcf5de01, 0xce1564c3df7d67ea, 0x178d1f6bd584afcc], + [0xce37e84fc9541b4b, 0x02a85a7410754529, 0xd7a1f169475b4ddf, 0xec6f34ceacaa73a0], + [0x9dd4ef421635b9dc, 0xc9816be068be66db, 0x625773cae69bbe05, 0x28221c36398c4c3a], + [0xac995c5066bdaaa5, 0x27d053c0f150fe10, 0xd6b48259b69e8530, 0x08daae840b2a43eb], + [0x6d69b1bd07837dc9, 0x451641e8455eedb6, 0x4624f85fe9b7cb83, 0xa1aa7f4a42808908], + [0x549786448c4f3f4d, 0xbaf30f4638d2231b, 0x46dab2eba04afd78, 0x68126a90b051062b], + [0x31f8514dc6115cb5, 0x8c641a6cb43ae7da, 0x8b8f45b58a12e6cc, 0x3242e1de1fa494c5], + [0xcd1bb864637cf4f9, 0x10e34d63f48c5ed4, 0x5b075be3c412ad03, 0xe6857f7fc3345385], + [0x47d58af32a91ddc2, 0x30abb6eab3f02921, 0x490df25e43f73f13, 0xf537773939d38a9b], + [0x0180bb9d84d7141b, 0x134911d8e4bbaaec, 0x4b92b8f64e804905, 0x897da7bb358b6d21], + [0x3d24524ac4d2c465, 0x0462308a30d9d3e5, 0x45254a01baae8a2e, 0xd2593661cf6daf12], + [0xec0ddc2b82cb0747, 0xdaeda7da80dde8ce, 0x98e2f9df22a96604, 0x8ab85f67eec67f06], + [0x2755c80bb1316dd9, 0x787ab24d5a604fb9, 0xdf25cd6e045b914d, 0x968513d803c0a611], + [0x35eaa6e36461d0d3, 0x17905f2a781de540, 0x2d531dd8552f2765, 0x01eeb7b6a1494475], + [0x828cc5c0f20462a9, 0x6d96e2a601b93c0e, 0x4cd09b8e39701ff1, 0xd909bb3be4c02cd8], + [0xc86610f34f66b3df, 0x015c9bf09e75950a, 0xcbde1f5982ace379, 0x6a13800155ef04b7], + [0x123308b4a9c5711a, 0x721dd5dacce5bc3d, 0x26804860af6a7e95, 0x7ccbc3bf87cc4d54], + [0xcd43853968e0f8c3, 0x9cab4fb10369ef34, 0xf5aba10ba06befde, 0x99ff4d8ff311e9a0], + [0x0c5516aa79811c1c, 0x8869b952943a29fc, 0xf382d60f67fcc2e2, 0xd3a606c601b5c66f], + [0x29dc4c21401d787f, 0x5c4a52f82ad10df3, 0x7048dcd10cdece63, 0x5eee8921f4fcd342], +]; + +/// `y` of `(2i + 1) G`, entry `i`. +pub(crate) const G_ODD_MULTIPLES_Y: [[u64; 4]; 32] = [ + [0x02df32e52139f0a0, 0xd0a9877cc62a4740, 0x59bdcee36b692153, 0xbc3736a2f4f6779c], + [0xaaacdd037458f6e6, 0x7c400ee5cd045292, 0xccc5cec08a72150f, 0x530b5dd88c688ef5], + [0xa6976eff5fbe6480, 0x5006206eb579ff7d, 0x4504c622b51cf38f, 0xf2df5db2d144e945], + [0x223b949657e52bc1, 0x037937707d6a49a2, 0x5cd6b6e9c12d2922, 0x847d18ffb38e8706], + [0x85227940922c02e9, 0xc3a8433140d1ebca, 0x768f7689b210f45f, 0x379e72f63722c924], + [0x739a8fd805174a4b, 0x0c94816e63c4bc72, 0x4918e5c02e2b0b93, 0x63516355287e39fe], + [0x03891e105e009b00, 0x58d1434ae934ba75, 0x1a473f8d9748f238, 0xe6bb9804458bb70f], + [0x4c3533771db0955e, 0xdc2e788fb170aa14, 0x5ee9fab985c12455, 0x32ec7722695dc7cf], + [0xe0180d54d4ae2221, 0x6cce6001d7c0b853, 0x3361493f76fac50f, 0x161e5851969da822], + [0xfd4520a63f5cc585, 0xb95f2de69200269e, 0x933ef442fca7a734, 0x96f361a23b3deb99], + [0xf945321148e90344, 0x60fcfd988fff6dec, 0x12a9423fd80dea54, 0x141caee612e7ab07], + [0x7bd8b7c847718ce5, 0x0e9b9643d766da33, 0x30c43be69b23cc14, 0xaef82cf361db1c71], + [0x9b499a7da7820bf9, 0x7021aafc29ac8e86, 0x53da45cec1caada1, 0xf575f34e5f6c63c6], + [0x73306a0ff0485872, 0xe607f33be20135ea, 0x6f251c63fbbd2927, 0x3c85cf62a744817e], + [0x2995225e66b162ba, 0x64c77a81858d4ca1, 0xab2012c41627cd81, 0x3b4d172d6963510b], + [0x2458b9e3c73bf657, 0xf7dc310560bbe83a, 0xc1b8e58150da662d, 0x80cc0a980495af87], + [0xc3c8bd75b5fe25de, 0x6e67fd81941c6a60, 0xeb15fccc63694830, 0x3d420dee447499cf], + [0x5a8ce092c6b7c016, 0xe7fec8984be51251, 0x35ecf208bdec5980, 0x7e72f70a2eaf1bf4], + [0x7dab7b373c8f551b, 0x56ab9bcf11b6f25a, 0xaa27b4380a8333ad, 0xcbf18ef2f229c025], + [0x1ca28a8ee15f1878, 0x5d72264551d68eae, 0x581dfa7d2b4c09e3, 0x6b61903f2ba03769], + [0xf9099fa3212d8fc1, 0x63ae367eb959d9f8, 0x8e2e511cce74be18, 0xbeb6132d28ff4099], + [0x38ecb2e51caf0aae, 0x6543e7e5a787f93d, 0xf571032510364cd2, 0x995b4a9a20d88c89], + [0x5dca599abe435bf0, 0xa169da8afcb3a620, 0x6cf13824af9f6508, 0x416b602161b2bbf6], + [0xe2874ab08c6c6e2b, 0x0445b6381fc45c42, 0x53e29b91b9afd253, 0x0d90c6f4584c9d15], + [0x60f9fd6e5a72901a, 0x9a31b57f2e430478, 0x893c927b9668ddd5, 0xb0992fa258261de1], + [0xb769488b3901a9f3, 0x7460ad9d8bcceeb9, 0x66a2abf1c7753bb8, 0xabd4a0f7fac6deb4], + [0x5eefa9a3ec3f9511, 0x05977fb39adacec7, 0x5bb65608aa16f24f, 0x10d8c2a3b3396bd8], + [0x1d092851b33025b7, 0x41d95d9e42639ac6, 0x84f5b14011d026db, 0x2de1cdf12e9cb8c0], + [0x5995334c3e242616, 0x3acfb676b987c213, 0x145518b80573d4c0, 0xc6ebde08aded9af4], + [0xd35aaa101d22c55a, 0xf2b3182557bf0529, 0xdd523fc5310a3ecf, 0x0b3cc5a35c82a6b7], + [0x0d6b519f677f9e84, 0x3ef8e988f9e9e6c0, 0x8726d00ed296c2b3, 0x16429acf62bb2d8f], + [0x9ffe119727f734ae, 0xf39e93dc1c8b27ad, 0x73516bbad0f603e4, 0xb6b3462b6a39a55b], +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::sm2::Sm2FieldElement; + use crate::sm2_domain::{G_X_LIMBS, G_Y_LIMBS}; + use crate::sm2_point::Sm2JacobianPoint; + + #[test] + fn table_matches_the_odd_multiples_of_g_computed_by_the_point_arithmetic() { + let g = Sm2JacobianPoint::from_affine( + Sm2FieldElement::from_limbs(G_X_LIMBS), + Sm2FieldElement::from_limbs(G_Y_LIMBS), + ); + let two_g = g.double(); + let mut cur = g; + for i in 0..32 { + let (x, y) = cur.to_affine().expect("an odd multiple of G below n is never infinity"); + assert_eq!(x.to_limbs(), G_ODD_MULTIPLES_X[i], "entry {i}: x of {}G", 2 * i + 1); + assert_eq!(y.to_limbs(), G_ODD_MULTIPLES_Y[i], "entry {i}: y of {}G", 2 * i + 1); + cur = cur.add_vartime(&two_g); + } + } +} diff --git a/crypto/ec/tests/barrett_tests.rs b/crypto/ec/tests/barrett_tests.rs new file mode 100644 index 000000000..39b617e88 --- /dev/null +++ b/crypto/ec/tests/barrett_tests.rs @@ -0,0 +1,130 @@ +//! Known-answer tests for [`bouncycastle_ec::barrett`], with `m = n - 1` for P-256 (the modulus +//! `bouncycastle-ecdsa`'s extra-bits reduction passes it). `MU_LOW_LIMBS` is +//! `floor(2^512 / m) - 2^256`, and every expected value is Python's `T % m`, both computed +//! independently of this crate; the inputs are the extremes of the algorithm's bound (`0`, `1`, +//! `m - 1`, `m`, `m + 1`, `2m - 1`, `4m - 1`, `m * 2^256 - 1`, the all-ones values at the widths +//! callers use) plus pseudorandom values (`random.seed(2026)`). + +use bouncycastle_ec::barrett::{limbs_from_be_bytes, reduce}; +use bouncycastle_ec::p256_scalar::N_LIMBS; + +const MU_LOW_LIMBS: [u64; 4] = + [0x012ffd85eedf9bff, 0x43190552df1a6c21, 0xfffffffeffffffff, 0x00000000ffffffff]; + +fn n_minus_1() -> [u64; 4] { + let mut m = N_LIMBS; + m[0] -= 1; + m +} + +#[test] +fn known_answers_at_the_bounds_extremes_and_pseudorandom_inputs() { + let m = n_minus_1(); + let cases: [([u64; 8], [u64; 4]); 12] = [ + ( + [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + ), + ( + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + ), + ( + [ + 0xf3b9cac2fc63254f, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + [0xf3b9cac2fc63254f, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000], + ), + ( + [ + 0xf3b9cac2fc632550, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + ), + ( + [ + 0xf3b9cac2fc632551, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + ), + ( + [ + 0xe7739585f8c64a9f, 0x79cdf55b4e2f3d09, 0xffffffffffffffff, 0xfffffffe00000001, + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + [0xf3b9cac2fc63254f, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000], + ), + ( + [ + 0xcee72b0bf18c953f, 0xf39beab69c5e7a13, 0xfffffffffffffffe, 0xfffffffc00000003, + 0x0000000000000003, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + [0xf3b9cac2fc63254f, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000], + ), + ( + [ + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0x00000000ffffffff, 0x0000000000000000, 0x0000000000000000, + ], + [0xe7739585f8c64a9f, 0x89b1054851cc17b9, 0x9c0166cd652e96b7, 0xfffffffe43190554], + ), + ( + [ + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xf3b9cac2fc63254f, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000, + ], + [0xf3b9cac2fc63254f, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000], + ), + ( + [ + 0x51c9bc701e7ea419, 0xf38b2ffc80a4df5a, 0xa5aec7978306d03b, 0xf3f49249dc28ff90, + 0xe255accb1a466884, 0x0000000039292d22, 0x0000000000000000, 0x0000000000000000, + ], + [0xdec46b5a9d169069, 0x638e03895206cd65, 0x6f3ad9656a29e7a3, 0xd511cdaaee884747], + ), + ( + [ + 0x99dd251de5121482, 0x8e7aa6e99f199504, 0xc88b28756bad6be2, 0x8c3d5f169293de8f, + 0xbb049a79d7a7a3cc, 0xc4a334bfc6cd75e9, 0xc0433cbd7dabe929, 0x00096263c5e818fa, + ], + [0xa2b4e9170b3fa0c2, 0x9109ae74fd54327f, 0xeac480d056cfea11, 0x7c84c22119b7a494], + ), + ( + [ + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0x00ffffffffffffff, + ], + [0xb156ab779217b88f, 0x15f6df373af24723, 0x1e2845b2382b6bec, 0xff66e12c97f3d957], + ), + ]; + for (t, expected) in cases { + assert_eq!(reduce::<4, 8, 5>(&t, &m, &MU_LOW_LIMBS), expected, "t = {t:x?}"); + } +} + +#[test] +fn limbs_from_be_bytes_packs_big_endian_input_little_endian_zero_extended() { + assert_eq!(limbs_from_be_bytes::<2>(&[]), [0, 0]); + assert_eq!(limbs_from_be_bytes::<2>(&[0x01]), [1, 0]); + assert_eq!(limbs_from_be_bytes::<2>(&[0x01, 0x02]), [0x0102, 0]); + assert_eq!( + limbs_from_be_bytes::<2>(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]), + [0x0203040506070809, 0x01] + ); + assert_eq!(limbs_from_be_bytes::<2>(&[0xff; 16]), [u64::MAX, u64::MAX]); +} + +#[test] +#[should_panic(expected = "do not fit")] +fn limbs_from_be_bytes_rejects_input_wider_than_the_limbs() { + let _ = limbs_from_be_bytes::<2>(&[0u8; 17]); +} diff --git a/crypto/ec/tests/bp256r1_comb_tests.rs b/crypto/ec/tests/bp256r1_comb_tests.rs new file mode 100644 index 000000000..12a198d8c --- /dev/null +++ b/crypto/ec/tests/bp256r1_comb_tests.rs @@ -0,0 +1,70 @@ +//! Known-answer tests for [`comb_multiply_base_point`] (`[k]G`), brainpoolP256r1. +//! +//! `k = 2, 3` reuse the values verified independently in `bp256r1_point_tests.rs`; `k` for +//! `known_answer_large_k` and `n-1` (where `[k]G = -G`) were computed the same way (Python affine +//! group law, general `a`), `random.seed(628256)` for the former. + +use bouncycastle_ec::bp256r1::Bp256r1FieldElement; +use bouncycastle_ec::bp256r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp256r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp256r1_scalar::Bp256r1Scalar; + +fn scalar(limbs: [u64; 4]) -> Bp256r1Scalar { + Bp256r1Scalar::from_limbs(limbs) +} + +fn assert_result_is(k_limbs: [u64; 4], x: [u64; 4], y: [u64; 4]) { + let result = comb_multiply_base_point(&scalar(k_limbs)); + let (rx, ry) = result.to_affine().expect("[k]G must not be infinity for k in (0, n)"); + assert_eq!(rx, Bp256r1FieldElement::from_limbs(x), "x mismatch for k = {k_limbs:x?}"); + assert_eq!(ry, Bp256r1FieldElement::from_limbs(y), "y mismatch for k = {k_limbs:x?}"); +} + +#[test] +fn known_answer_small_k() { + assert_result_is([1, 0, 0, 0], G_X_LIMBS, G_Y_LIMBS); + assert_result_is( + [2, 0, 0, 0], + [0xd51a14c2ce13ea0e, 0x36ef044166699e37, 0xb55f8aa369593ac4, 0x743cf1b8b5cd4f2e], + [0x892ada097eeb7cd4, 0x38df059f69249406, 0x946fe0bb776529da, 0x36ed163337deba9c], + ); + assert_result_is( + [3, 0, 0, 0], + [0x6b91e2ad25cae39d, 0xd2aa843d0c0fca01, 0xd6624c3ab4f6cc16, 0xa8f217b77338f1d4], + [0x7e65cc5602b74f9d, 0xfac10e4589348fb7, 0x0aa2a6850a1b40f5, 0x4b49cafc7dac26bb], + ); +} + +#[test] +fn known_answer_large_k() { + let k: [u64; 4] = + [0x212709183943ca3e, 0x544272ddbf740a68, 0x6b35d760af6377e2, 0x7e72f2a30e3279c2]; + assert_result_is( + k, + [0xd5926c7536cd5b6f, 0x43fd851180cd5eb6, 0x53935733164d2188, 0x6bfea2ac64a53e7d], + [0xbf7d9c2e9e6a16c7, 0x7ab99319bfc3bd9a, 0xd9ea86a322cd326b, 0xa7ccb8b1348e5931], + ); +} + +#[test] +fn edge_case_k_equals_n_minus_1() { + // (n-1)*G == -G: same x as G, negated y. + let n_minus_1: [u64; 4] = + [0x901e0e82974856a6, 0x8c397aa3b561a6f7, 0x3e660a909d838d71, 0xa9fb57dba1eea9bc]; + assert_result_is( + n_minus_1, + G_X_LIMBS, + [0xc3f5f355f069e9e0, 0xabc4b110a73891d3, 0xa66dc47689226fa8, 0x557c5fa5de13e4be], + ); +} + +/// `k = 0` is the one scalar for which `[k]G` is the identity: every comb digit is `0`, so every +/// round selects table entry `0` (the infinity sentinel) and the accumulator never leaves +/// `INFINITY`. Nothing in the signing path can produce it (`k` is in `[1, n-1]` by construction), +/// which is exactly why it needs pinning here rather than being left to chance. +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let result = comb_multiply_base_point(&scalar([0u64; 4])); + assert!(result.is_infinity().to_bool()); + assert!(result.to_affine().is_none()); +} diff --git a/crypto/ec/tests/bp256r1_extra_bits_tests.rs b/crypto/ec/tests/bp256r1_extra_bits_tests.rs new file mode 100644 index 000000000..53475e26d --- /dev/null +++ b/crypto/ec/tests/bp256r1_extra_bits_tests.rs @@ -0,0 +1,142 @@ +//! Known-answer tests for [`reduce_wide_bits_mod_n_minus_1`] (FIPS 186-5 Appendix A.4.1 steps +//! 3-5): `x = bits2int(X)`, `x = x mod (n-1)`, `x = x + 1`. Expected values computed independently +//! in Python via `(int.from_bytes(bytes, 'big') % (n - 1)) + 1` with `n` from RFC 5639 §3.4, not +//! derived from this crate's own reduction code -- existing round-trip tests elsewhere +//! (`keygen`/`sign_rng`) only ever check that the reduced value is self-consistent with +//! itself, which can't distinguish a correct reduction from a systematically wrong one. This is +//! exactly what a `cargo mutants` run on `reduce_wide_bits_mod_n_minus_1` and its +//! `N_MINUS_1_LIMBS` constant surfaced: several bit-level and off-by-one mutants survived because +//! nothing here pinned the function (or the constant) against an independent reference. + +use bouncycastle_ec::bp256r1_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_hex::decode as hex_decode; + +fn bytes32(hex: &str) -> [u8; 32] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +#[test] +fn known_answer_repeating_pattern() { + let input: Vec = [0x01u8, 0x02, 0x03, 0x04].iter().cloned().cycle().take(40).collect(); + let expected = bytes32("0880c22fd57c42d70627f928514f43559a297f84345376cb9594d539b99bcda1"); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_ff() { + let input = [0xffu8; 40]; + let expected = bytes32("7c77671078e3be32ef1eb4b986f3910565290994ebcb0ac14b07eb03bfda584c"); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_zero_yields_one() { + // bits2int(0) = 0; 0 mod (n-1) = 0; +1 = 1 -- the smallest value Appendix A.4.1 can produce. + let input = [0u8; 40]; + let mut expected = [0u8; 32]; + expected[31] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +/// The two boundaries of Appendix A.4.1's own arithmetic: `n - 1` is the modulus of step 4, so it +/// reduces to `0` and step 5 makes it `1`; `n - 2` is the largest residue, so it comes out as +/// `n - 1`, the top of the `[1, n-1]` output interval. Both inputs are left-padded to the DRBG +/// output width this module is fed with. +#[test] +fn known_answer_at_the_n_minus_1_boundary() { + let n = bouncycastle_ec::bp256r1_sec1::be_bytes_from_limbs( + &bouncycastle_ec::bp256r1_scalar::N_LIMBS, + ); + let mut n_minus_1 = n; + n_minus_1[31] -= 1; // n is odd, so neither of these subtractions borrows + let mut n_minus_2 = n; + n_minus_2[31] -= 2; + + let mut input = [0u8; 40]; + input[8..].copy_from_slice(&n_minus_1); + let mut one = [0u8; 32]; + one[31] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), one, "n - 1 -> 1"); + + input[8..].copy_from_slice(&n_minus_2); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), n_minus_1, "n - 2 -> n - 1"); +} + +/// The bit-serial algorithm this module implemented before switching to Barrett reduction, kept +/// here as an independent reference: Horner's rule over the input bits, doubling the running +/// remainder and subtracting `n - 1` when it is exceeded, with the carry out of the top limb folded +/// back in as `2^256 mod (n-1) = 2^256 - (n-1)`. Slow, obviously correct, and sharing no +/// code with the Barrett path beyond `nat`'s add and sub. +fn reference_bit_serial(bytes: &[u8]) -> [u8; 32] { + use bouncycastle_ec::nat; + let n = bouncycastle_ec::bp256r1_scalar::N_LIMBS; + let mut m = n; + m[0] -= 1; // n is odd + let two_pow_mod_m = nat::sub(&[0u64; 4], &m).0; // 2^256 - m, wrapping + let mut acc = [0u64; 4]; + for &byte in bytes { + for bit_idx in (0..8).rev() { + let (mut doubled, carry) = nat::add(&acc, &acc); + doubled[0] |= ((byte >> bit_idx) & 1) as u64; + if carry == 1 { + doubled = nat::add(&doubled, &two_pow_mod_m).0; + } + let (reduced, borrow) = nat::sub(&doubled, &m); + acc = if borrow == 1 { doubled } else { reduced }; + } + } + let plus_one = nat::add(&acc, &[1u64, 0, 0, 0]).0; + bouncycastle_ec::bp256r1_sec1::be_bytes_from_limbs(&plus_one) +} + +/// xorshift64*, fixed seed, as in the `ec` crate's property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } +} + +/// Barrett against the bit-serial reference over pseudorandom inputs at every width from one byte +/// up to the 63-byte maximum the function accepts, biased towards the widths actually drawn +/// (40 bytes) and towards values with long runs of set bits, where Barrett's quotient estimate is +/// furthest off and the most conditional subtractions are needed. +#[test] +fn agrees_with_the_bit_serial_reference_over_many_pseudorandom_inputs() { + let mut rng = Xorshift64(0xBA77E770_0000_0004); + for i in 0..3000 { + let len = match i % 4 { + 0 => 40, + 1 => 63, + _ => 1 + (rng.next_u64() as usize % 63), + }; + let mut bytes = vec![0u8; len]; + for chunk in bytes.chunks_mut(8) { + let word = rng.next_u64().to_be_bytes(); + chunk.copy_from_slice(&word[..chunk.len()]); + } + if i % 3 == 0 { + for b in bytes.iter_mut().take(len / 2) { + *b = 0xff; + } + } + assert_eq!( + reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), + reference_bit_serial(&bytes), + "len = {len}, bytes = {bytes:02x?}" + ); + } +} + +#[test] +fn maximum_width_all_ones_input() { + // 2^504 - 1, the largest input accepted: Barrett's precondition holds with room to spare. + let bytes = [0xffu8; 63]; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), reference_bit_serial(&bytes)); +} diff --git a/crypto/ec/tests/bp256r1_field_tests.rs b/crypto/ec/tests/bp256r1_field_tests.rs new file mode 100644 index 000000000..2c8a3a077 --- /dev/null +++ b/crypto/ec/tests/bp256r1_field_tests.rs @@ -0,0 +1,306 @@ +//! Known-answer tests for [`Bp256r1FieldElement`] arithmetic. Expected values computed +//! independently in Python: +//! +//! ```text +//! p = 0xa9fb57dba1eea9bc3e660a909d838d726e3bf623d52620282013481d1f6e5377 +//! random.seed(2560256) +//! vals = [random.randrange(1, p) for _ in range(3)] +//! ``` + +use bouncycastle_ec::bp256r1::Bp256r1FieldElement; + +const VALS_0: [u64; 4] = + [0xb4b76e9c3e0696d7, 0x6978c023e8559edd, 0x9929473357b901b0, 0x52ab03860069228e]; +const VALS_1: [u64; 4] = + [0x2d3cc3f5bb18a119, 0x2e88e3651dfa0ef6, 0x09778cd067fad368, 0x61d733f9016bb161]; +const VALS_2: [u64; 4] = + [0x124a5bf60cbb67a0, 0x521bc0efbdd51add, 0xe37d905da3bd5b7e, 0x97b610bcd35b4d14]; + +const ADD_0_1: [u64; 4] = + [0xc1e0ea74d9b0e479, 0x29c5ad6531298dab, 0x643ac973223047a6, 0x0a86dfa35fe62a33]; +const SUB_0_1: [u64; 4] = + [0xa78df2c3a25c4935, 0xa92bd2e29f81b00f, 0xce17c4f38d41bbba, 0x9acf2768a0ec1ae9]; +const MUL_0_1: [u64; 4] = + [0x996756f13a9f75c2, 0x9ee4cb0249a9601e, 0x2fb3949c468534ed, 0x9dd8a748da7081fc]; +const ADD_0_2: [u64; 4] = + [0xa6ee82752b53ab00, 0x4d588aefd1049992, 0x3e40cd005df2cfbc, 0x4065bc6731d5c5e7]; +const SUB_0_2: [u64; 4] = + [0xc2805ac350b982ae, 0x8598f557ffa6a428, 0xf411c166517f33a4, 0x64f04aa4cefc7f35]; +const MUL_0_2: [u64; 4] = + [0x7b8209c92be4a50b, 0x90d24caa244a2bbc, 0x0a31c9ecd85c36cd, 0x2907d8ce540f36e0]; +const ADD_1_2: [u64; 4] = + [0x1f73d7cea865b542, 0x1268ae3106a909ab, 0xae8f129d6e34a174, 0x4f91ecda32d854b9]; +const SUB_1_2: [u64; 4] = + [0x3b05b01ccdcb8cf0, 0x4aa91899354b1441, 0x6460070361c1055c, 0x741c7b17cfff0e08]; +const MUL_1_2: [u64; 4] = + [0xea94aa665f314b4d, 0x3735e3f1265d9a91, 0x3608b85373a98110, 0x2e3e5cade58a3691]; + +const NEG_0: [u64; 4] = + [0x6b5bd980e167bca0, 0x04c335ffecd0814a, 0xa53cc35d45ca8bc2, 0x57505455a185872d]; +const INV_0: [u64; 4] = + [0x024bfbb6bb50057e, 0x6999cac61dc2af6c, 0xb2db58c58538fe5d, 0x86005a26300178a7]; +const NEG_1: [u64; 4] = + [0xf2d684276455b25e, 0x3fb312beb72c1131, 0x34ee7dc03588ba0a, 0x482423e2a082f85b]; +const INV_1: [u64; 4] = + [0x19338439822f7dcb, 0x30c88a4091b117f2, 0xf89059499b3a1310, 0x10f0efcb8f77da09]; +const NEG_2: [u64; 4] = + [0x0dc8ec2712b2ebd7, 0x1c2035341751054b, 0x5ae87a32f9c631f4, 0x1245471ece935ca7]; +const INV_2: [u64; 4] = + [0x5c40b4ad887af6ac, 0x9149a743b580ec29, 0xe421eaa4e216daab, 0x60e61eb16f319668]; + +fn fe(limbs: [u64; 4]) -> Bp256r1FieldElement { + Bp256r1FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + let pairs: [(usize, usize, [u64; 4], [u64; 4], [u64; 4]); 3] = [ + (0, 1, ADD_0_1, SUB_0_1, MUL_0_1), + (0, 2, ADD_0_2, SUB_0_2, MUL_0_2), + (1, 2, ADD_1_2, SUB_1_2, MUL_1_2), + ]; + for (i, j, a, s, m) in pairs { + assert_eq!(vals[i].add(&vals[j]), fe(a), "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), fe(a), "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), fe(s), "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), fe(m), "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), fe(m), "mul is commutative ({j},{i})"); + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2]; + let negs = [NEG_0, NEG_1, NEG_2]; + let invs = [INV_0, INV_1, INV_2]; + for i in 0..3 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + for &v in &vals { + assert_eq!(v.add(&Bp256r1FieldElement::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&Bp256r1FieldElement::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), Bp256r1FieldElement::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), Bp256r1FieldElement::ZERO, "x + (-x) == 0"); + assert_eq!(v.mul(&v.invert()), Bp256r1FieldElement::ONE, "x * x^-1 == 1"); + } + assert_eq!( + Bp256r1FieldElement::ZERO.invert(), + Bp256r1FieldElement::ZERO, + "0^-1 == 0 by convention" + ); +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..4 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + use bouncycastle_ec::bp256r1::P_LIMBS; + // p itself must reduce to 0, and p+1 to 1. + assert_eq!(fe(P_LIMBS), Bp256r1FieldElement::ZERO); + assert_eq!( + fe([0x2013481d1f6e5378, 0x6e3bf623d5262028, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc]), + Bp256r1FieldElement::ONE + ); + + // The maximum limb pattern, 2^256 - 1, is the widest input the single conditional + // subtraction has to cover (it is < 2p, since p > 2^255); expected value is + // 2^256 - 1 - p, computed in Python. + assert_eq!( + fe([u64::MAX; 4]), + fe([0xdfecb7e2e091ac88, 0x91c409dc2ad9dfd7, 0xc199f56f627c728d, 0x5604a8245e115643]) + ); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the P-256 field test's property check. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0xB5A11E5A11E5B5A1); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != Bp256r1FieldElement::ZERO { + assert_eq!(a.mul(&a.invert()), Bp256r1FieldElement::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE0000000004); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 4], + { + let mut l = [0u64; 4]; + l[0] = 1; + l + }, + [u64::MAX; 4], + { + let mut l = [u64::MAX; 4]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `is_zero` is the mask every exceptional-case select in this crate's point arithmetic keys +/// off (infinity detection, the `H == 0` / `R == 0` same-point and opposite-point cases), and +/// otherwise exercised only through those selects. Both truth values are pinned here, with +/// the nonzero side walked across every limb position so a mask that inspected only some limbs +/// would be caught. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(Bp256r1FieldElement::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::bp256r1::P_LIMBS).is_zero().to_bool(), "p reduces to 0"); + assert!(!Bp256r1FieldElement::ONE.is_zero().to_bool()); + for limb_idx in 0..4 { + let mut limbs = [0u64; 4]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `add`'s carry-out correction and `sub`'s borrow correction, pinned at the operands that force +/// them: `(p-1) + (p-1)` is the largest sum two canonical elements can form, and `0 - 1` the +/// smallest difference. The expected values are `p - 2` and `p - 1` themselves (computed from +/// `p` in Python), so this is a check of the wrap-around handling, not of the digits of `p`. +#[test] +fn known_answer_add_carry_and_sub_borrow_at_the_extremes() { + let p_minus_1 = + fe([0x2013481d1f6e5376, 0x6e3bf623d5262028, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc]); + let p_minus_2 = + fe([0x2013481d1f6e5375, 0x6e3bf623d5262028, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc]); + let two = fe([0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]); + + assert_eq!(p_minus_1.add(&p_minus_1), p_minus_2, "(p-1) + (p-1) == p-2"); + assert_eq!( + p_minus_1.add(&Bp256r1FieldElement::ONE), + Bp256r1FieldElement::ZERO, + "(p-1) + 1 == 0" + ); + assert_eq!(Bp256r1FieldElement::ZERO.sub(&Bp256r1FieldElement::ONE), p_minus_1, "0 - 1 == p-1"); + assert_eq!(Bp256r1FieldElement::ONE.sub(&p_minus_1), two, "1 - (p-1) == 2"); + assert_eq!(p_minus_1.negate(), Bp256r1FieldElement::ONE, "-(p-1) == 1"); + assert_eq!(Bp256r1FieldElement::ONE.invert(), Bp256r1FieldElement::ONE, "1^-1 == 1"); + assert_eq!(p_minus_1.invert(), p_minus_1, "(p-1)^-1 == p-1, since (p-1)^2 == 1"); +} + +/// Products at the reduction's extremes rather than in the middle of its range, the same class +/// of check `p256_field_tests.rs`/`p384_field_tests.rs`/`sm2_field_tests.rs` already carry and +/// this curve did not: `(p-1)^2` and `(p-1)(p-2)` (the largest products two canonical elements +/// can form), operands within `2^64` of `p`, and operands with the top bit set. Expected values +/// are Python's `(a * b) % p`, computed independently of this crate's arithmetic +/// (`random.seed(20260918)` for the pseudorandom operands). +#[test] +fn known_answer_mul_at_the_reduction_bounds() { + let cases: [([u64; 4], [u64; 4], [u64; 4]); 7] = [ + // (p-1)^2 == 1 + ( + [0x2013481d1f6e5376, 0x6e3bf623d5262028, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc], + [0x2013481d1f6e5376, 0x6e3bf623d5262028, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc], + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + ), + // (p-1)(p-2) == 2 + ( + [0x2013481d1f6e5376, 0x6e3bf623d5262028, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc], + [0x2013481d1f6e5375, 0x6e3bf623d5262028, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc], + [0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + ), + // (p-1) * 2^(bits-1) == p - 2^(bits-1) + ( + [0x2013481d1f6e5376, 0x6e3bf623d5262028, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc], + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x8000000000000000], + [0x2013481d1f6e5377, 0x6e3bf623d5262028, 0x3e660a909d838d72, 0x29fb57dba1eea9bc], + ), + // both operands within 2^64 of p + ( + [0x42f16588ab38db81, 0x6e3bf623d5262027, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc], + [0xb88cd6435e191cfd, 0x6e3bf623d5262027, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc], + [0x8ee275f9f3770f3c, 0x596cc43e62331583, 0x0000000000000000, 0x0000000000000000], + ), + // both operands within 2^64 of p + ( + [0x7e6bee23576cad67, 0x6e3bf623d5262027, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc], + [0xeb8178c615941584, 0x6e3bf623d5262027, 0x3e660a909d838d72, 0xa9fb57dba1eea9bc], + [0x55edac6f37c27130, 0x213210fdfaf3dd11, 0x0000000000000000, 0x0000000000000000], + ), + // both operands with the top bit set + ( + [0x35a4fd8d5d0e0d44, 0x009a7390e7e34e82, 0x5d1f1b0f6be8d021, 0xa839e4d3b95980af], + [0x5eb8baefb2b23ce5, 0x8409e6f65dd90b53, 0x9f0b5ec8807db146, 0x9fa88c95a7b0b5b2], + [0xf9b64af6ceabe3b5, 0xaadf5792205e4ecc, 0xd860664d6031cc11, 0x7f1499cc8fd2f83e], + ), + // both operands with the top bit set + ( + [0x1108e28819183525, 0x5f4239f80228423b, 0xd816dfd7771d5f7d, 0xa1fafce172bc1011], + [0x913ab474603f4513, 0x3885cc6ffcb2a055, 0xe4fdc11a9ab24157, 0xa52f75124f2ff581], + [0x90e40a31c6528827, 0xc40c43b69f963a52, 0x4a5b250f463bf3f7, 0x1f336e46db83d34a], + ), + ]; + for (a, b, expected) in cases { + assert_eq!(fe(a).mul(&fe(b)), fe(expected), "a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(b).mul(&fe(a)), fe(expected), "commuted: a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(a).square(), fe(a).mul(&fe(a)), "square at the same extreme: a = {a:x?}"); + } +} diff --git a/crypto/ec/tests/bp256r1_mul_tests.rs b/crypto/ec/tests/bp256r1_mul_tests.rs new file mode 100644 index 000000000..f5b7ea1c0 --- /dev/null +++ b/crypto/ec/tests/bp256r1_mul_tests.rs @@ -0,0 +1,172 @@ +//! Behavioural tests for [`multiply_point`] (`[k]Q`, constant time in `k`): cross-checked against +//! the fixed-base comb for `Q = G`, against the variable-time wNAF multiplier for a random `Q`, +//! and against `[a k mod n]G` for `Q = [a]G` (three multipliers of independent design computing +//! the same point), plus the edge cases a windowed multiplier can get wrong: `k = 0`, `k = 1`, +//! `k = n-1`, a window of all ones, `Q` the identity, `Q` negated, and `Q` given in a +//! non-canonical-`Z` Jacobian representation. + +use bouncycastle_ec::bp256r1::Bp256r1FieldElement; +use bouncycastle_ec::bp256r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp256r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp256r1_mul::multiply_point; +use bouncycastle_ec::bp256r1_point::Bp256r1JacobianPoint; +use bouncycastle_ec::bp256r1_scalar::{ + Bp256r1PublicScalar, Bp256r1Scalar, Bp256r1ScalarField, N_LIMBS, +}; +use bouncycastle_ec::bp256r1_wnaf::shamir_multiply; +use bouncycastle_ec::nat; + +const LIMBS: usize = 4; + +fn g() -> Bp256r1JacobianPoint { + Bp256r1JacobianPoint::from_affine( + Bp256r1FieldElement::from_limbs(G_X_LIMBS), + Bp256r1FieldElement::from_limbs(G_Y_LIMBS), + ) +} + +fn zero() -> [u64; LIMBS] { + [0; LIMBS] +} + +fn small(v: u64) -> [u64; LIMBS] { + let mut limbs = zero(); + limbs[0] = v; + limbs +} + +fn one() -> [u64; LIMBS] { + small(1) +} + +fn n_minus_1() -> [u64; LIMBS] { + let (limbs, borrow) = nat::sub(&N_LIMBS, &one()); + assert_eq!(borrow, 0); + limbs +} + +/// A pseudorandom value reduced into `[0, n)`, so it is a valid scalar for every multiplier. +fn reduced(limbs: [u64; LIMBS]) -> [u64; LIMBS] { + Bp256r1ScalarField::from_limbs(limbs).to_limbs() +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; LIMBS] { + let mut limbs = zero(); + for limb in limbs.iter_mut() { + *limb = self.next_u64(); + } + limbs + } +} + +#[test] +fn k_times_g_agrees_with_the_comb_multiplier() { + let mut rng = Xorshift64(0x9E3779B97F4A7C15); + let mut cases = vec![one(), small(2), small(3), small(15), small(16), small(17)]; + cases.push(n_minus_1()); + let mut n_minus_2 = n_minus_1(); + n_minus_2[0] -= 1; + cases.push(n_minus_2); + for _ in 0..50 { + cases.push(reduced(rng.next_limbs())); + } + for k in cases { + let via_mul = multiply_point(&Bp256r1Scalar::from_limbs(k), &g()); + let via_comb = comb_multiply_base_point(&Bp256r1Scalar::from_limbs(k)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "k = {k:x?}"); + } +} + +#[test] +fn k_times_q_agrees_with_the_wnaf_multiplier_and_with_the_scalar_field() { + let mut rng = Xorshift64(0xC2B2AE3D27D4EB4F); + for _ in 0..50 { + let a = reduced(rng.next_limbs()); + let k = reduced(rng.next_limbs()); + let q = comb_multiply_base_point(&Bp256r1Scalar::from_limbs(a)); + + let via_mul = multiply_point(&Bp256r1Scalar::from_limbs(k), &q); + + // [0]G + [k]Q by the variable-time multiplier. + let via_wnaf = shamir_multiply( + &Bp256r1PublicScalar::from_limbs(zero()), + &Bp256r1PublicScalar::from_limbs(k), + &q, + ); + assert_eq!(via_mul.to_affine(), via_wnaf.to_affine(), "wnaf: a = {a:x?}, k = {k:x?}"); + + // [a k mod n]G by the fixed-base multiplier. + let ak = + Bp256r1ScalarField::from_limbs(a).mul(&Bp256r1ScalarField::from_limbs(k)).to_limbs(); + let via_comb = comb_multiply_base_point(&Bp256r1Scalar::from_limbs(ak)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "comb: a = {a:x?}, k = {k:x?}"); + } +} + +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let q = g().double(); + assert!(multiply_point(&Bp256r1Scalar::from_limbs(zero()), &q).is_infinity().to_bool()); +} + +#[test] +fn edge_case_k_equals_one_is_q() { + let q = g().double(); + assert_eq!(multiply_point(&Bp256r1Scalar::from_limbs(one()), &q).to_affine(), q.to_affine()); +} + +#[test] +fn edge_case_k_equals_n_minus_1_is_minus_q() { + let q = g().double().add(&g()); + let result = multiply_point(&Bp256r1Scalar::from_limbs(n_minus_1()), &q); + assert_eq!(result.to_affine(), q.negate().to_affine()); +} + +#[test] +fn edge_case_q_is_infinity() { + let k = reduced(Xorshift64(0x2545F4914F6CDD1D).next_limbs()); + assert!( + multiply_point(&Bp256r1Scalar::from_limbs(k), &Bp256r1JacobianPoint::INFINITY) + .is_infinity() + .to_bool() + ); +} + +#[test] +fn negating_q_negates_the_result() { + let k = reduced(Xorshift64(0x1234_5678_9ABC_DEF1).next_limbs()); + let q = g().double(); + let kq = multiply_point(&Bp256r1Scalar::from_limbs(k), &q); + let k_neg_q = multiply_point(&Bp256r1Scalar::from_limbs(k), &q.negate()); + assert_eq!(k_neg_q.to_affine(), kq.negate().to_affine()); +} + +#[test] +fn q_in_a_non_canonical_z_representation_gives_the_same_result() { + // (X, Y, Z) and (λ²X, λ³Y, λZ) are the same affine point; the multiplier must treat them alike. + let k = reduced(Xorshift64(0xDEADBEEFCAFEF00D).next_limbs()); + let q = g().double(); + let lambda = Bp256r1FieldElement::from_limbs(small(7)); + let lambda2 = lambda.mul(&lambda); + let lambda3 = lambda2.mul(&lambda); + let scaled = + Bp256r1JacobianPoint { x: q.x.mul(&lambda2), y: q.y.mul(&lambda3), z: q.z.mul(&lambda) }; + assert_eq!(scaled.to_affine(), q.to_affine(), "the scaled point is the same point"); + assert_eq!( + multiply_point(&Bp256r1Scalar::from_limbs(k), &scaled).to_affine(), + multiply_point(&Bp256r1Scalar::from_limbs(k), &q).to_affine() + ); +} diff --git a/crypto/ec/tests/bp256r1_point_tests.rs b/crypto/ec/tests/bp256r1_point_tests.rs new file mode 100644 index 000000000..0c74ffeaa --- /dev/null +++ b/crypto/ec/tests/bp256r1_point_tests.rs @@ -0,0 +1,107 @@ +//! Known-answer tests for [`Bp256r1JacobianPoint`] arithmetic. +//! +//! `G`, `2G`, `3G` were computed independently in Python via the standard affine group law +//! (general `a`), not from this crate, and cross-checked against `n*G == infinity`: +//! +//! ```text +//! p = 0xA9FB57DBA1EEA9BC3E660A909D838D726E3BF623D52620282013481D1F6E5377 +//! a = 0x7D5A0975FC2C3057EEF67530417AFFE7FB8055C126DC5C6CE94A4B44F330B5D9 +//! # affine add/double via the standard general-a formulas +//! ``` + +use bouncycastle_ec::bp256r1::Bp256r1FieldElement; +use bouncycastle_ec::bp256r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp256r1_point::Bp256r1JacobianPoint; + +const TWO_G_X: [u64; 4] = + [0xd51a14c2ce13ea0e, 0x36ef044166699e37, 0xb55f8aa369593ac4, 0x743cf1b8b5cd4f2e]; +const TWO_G_Y: [u64; 4] = + [0x892ada097eeb7cd4, 0x38df059f69249406, 0x946fe0bb776529da, 0x36ed163337deba9c]; +const THREE_G_X: [u64; 4] = + [0x6b91e2ad25cae39d, 0xd2aa843d0c0fca01, 0xd6624c3ab4f6cc16, 0xa8f217b77338f1d4]; +const THREE_G_Y: [u64; 4] = + [0x7e65cc5602b74f9d, 0xfac10e4589348fb7, 0x0aa2a6850a1b40f5, 0x4b49cafc7dac26bb]; + +fn fe(limbs: [u64; 4]) -> Bp256r1FieldElement { + Bp256r1FieldElement::from_limbs(limbs) +} + +fn g() -> Bp256r1JacobianPoint { + Bp256r1JacobianPoint::from_affine(fe(G_X_LIMBS), fe(G_Y_LIMBS)) +} + +#[test] +fn known_answer_double() { + let doubled = g().double(); + let (x, y) = doubled.to_affine().unwrap(); + assert_eq!(x, fe(TWO_G_X)); + assert_eq!(y, fe(TWO_G_Y)); +} + +#[test] +fn known_answer_add() { + let sum = g().add(&g().double()); + let (x, y) = sum.to_affine().unwrap(); + assert_eq!(x, fe(THREE_G_X)); + assert_eq!(y, fe(THREE_G_Y)); +} + +#[test] +fn point_plus_negation_is_infinity() { + let g = g(); + assert!(g.add(&g.negate()).is_infinity().to_bool()); +} + +#[test] +fn infinity_identities() { + let g = g(); + let infinity = Bp256r1JacobianPoint::INFINITY; + assert!(infinity.is_infinity().to_bool()); + assert!(infinity.to_affine().is_none()); + + let (x1, y1) = g.add(&infinity).to_affine().unwrap(); + let (x2, y2) = g.to_affine().unwrap(); + assert_eq!((x1, y1), (x2, y2), "P + infinity == P"); + + let (x3, y3) = infinity.add(&g).to_affine().unwrap(); + assert_eq!((x3, y3), (x2, y2), "infinity + P == P"); + + assert!(infinity.add(&infinity).is_infinity().to_bool()); +} + +#[test] +fn scaled_z_representation_still_adds_correctly() { + // (X, Y, Z) and (X*c^2, Y*c^3, Z*c) represent the same affine point for any nonzero c; the + // branch-free add/double formulas must agree regardless of which representative is used. + let c = fe([7, 0, 0, 0]); + let c2 = c.mul(&c); + let c3 = c2.mul(&c); + let g = g(); + let scaled = Bp256r1JacobianPoint { x: g.x.mul(&c2), y: g.y.mul(&c3), z: g.z.mul(&c) }; + + let canonical_double = g.double().to_affine().unwrap(); + let scaled_double = scaled.double().to_affine().unwrap(); + assert_eq!(canonical_double, scaled_double); + + let canonical_sum = g.add(&g.double()).to_affine().unwrap(); + let scaled_sum = scaled.add(&g.double()).to_affine().unwrap(); + assert_eq!(canonical_sum, scaled_sum); +} + +/// `is_infinity` was previously only ever asserted true. The false side is pinned on the base +/// point and on values derived from it, and doubling the identity must stay at the identity +/// (`double` is total, per its docs, and the comb multiplier's accumulator starts there). +#[test] +fn is_infinity_is_false_for_finite_points_and_infinity_doubles_to_itself() { + let g = g(); + assert!(!g.is_infinity().to_bool(), "G is not the identity"); + assert!(!g.double().is_infinity().to_bool(), "2G is not the identity"); + assert!(!g.negate().is_infinity().to_bool(), "-G is not the identity"); + assert!(!g.add(&g.double()).is_infinity().to_bool(), "3G is not the identity"); + + let inf = Bp256r1JacobianPoint::INFINITY; + assert!(inf.double().is_infinity().to_bool(), "2 * infinity == infinity"); + assert!(inf.double().to_affine().is_none()); + assert!(inf.negate().is_infinity().to_bool(), "-infinity == infinity"); + assert!(inf.double().add(&g).to_affine().is_some(), "2 * infinity + G == G, a finite point"); +} diff --git a/crypto/ec/tests/bp256r1_scalar_tests.rs b/crypto/ec/tests/bp256r1_scalar_tests.rs new file mode 100644 index 000000000..c14fc1b71 --- /dev/null +++ b/crypto/ec/tests/bp256r1_scalar_tests.rs @@ -0,0 +1,461 @@ +//! Known-answer tests for [`Bp256r1ScalarField`] arithmetic mod the brainpoolP256r1 curve order +//! `n`. Expected values computed independently in Python: +//! +//! ```text +//! n = 0xA9FB57DBA1EEA9BC3E660A909D838D718C397AA3B561A6F7901E0E82974856A7 +//! random.seed(830256) +//! vals = [random.randrange(1, n) for _ in range(5)] + [0] +//! # for each pair (i, j) with i < j: (vals[i]+vals[j]) % n, (vals[i]-vals[j]) % n, (vals[i]*vals[j]) % n +//! # for each i: (-vals[i]) % n, pow(vals[i], n-2, n) if vals[i] != 0 else 0 +//! ``` + +use bouncycastle_ec::bp256r1_scalar::{ + Bp256r1PublicScalar, Bp256r1Scalar, Bp256r1ScalarField, N_LIMBS, +}; + +const VALS_0: [u64; 4] = + [0xe620e95875efcc3f, 0x700c3605fe836b66, 0xe0cbcb49c993c600, 0x4bdef4899a7f5646]; +const VALS_1: [u64; 4] = + [0x873deeeb190e25ff, 0xd4d7aa54b1fa9ff4, 0xc05f5da448731369, 0x3e1a9d4df469bb69]; +const VALS_2: [u64; 4] = + [0x3ec10068454f360e, 0x80c71b38a1bfcb59, 0x3520b45d1fad38df, 0x4e1f91f936074c2c]; +const VALS_3: [u64; 4] = + [0x6f5d2ca4d7fbf544, 0x2dcbd43dd421b47e, 0xecd09a4a451a1170, 0x840aefc083787afa]; +const VALS_4: [u64; 4] = + [0xe354fd165a775408, 0xa3642d3d28489575, 0xcd328ff0de425666, 0x153500b5078a85e0]; +const VALS_5: [u64; 4] = + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; + +const ADD_0_1: [u64; 4] = + [0x6d5ed8438efdf23e, 0x44e3e05ab07e0b5b, 0xa12b28ee1206d96a, 0x89f991d78ee911b0]; +const SUB_0_1: [u64; 4] = + [0x5ee2fa6d5ce1a640, 0x9b348bb14c88cb72, 0x206c6da58120b296, 0x0dc4573ba6159add]; +const MUL_0_1: [u64; 4] = + [0x13701529399da18c, 0x13d6a7943115c063, 0x6205ff1e0cd8b30b, 0x5147c3b6f9b973a3]; +const ADD_0_2: [u64; 4] = + [0x24e1e9c0bb3f024d, 0xf0d3513ea04336c0, 0x15ec7fa6e940fedf, 0x99fe8682d086a273]; +const SUB_0_2: [u64; 4] = + [0x377df772c7e8ecd8, 0x7b7e957112254705, 0xea11217d476a1a92, 0xa7baba6c0666b3d6]; +const MUL_0_2: [u64; 4] = + [0xd3360dd3e1591b94, 0x472f60b5a2625271, 0xcc6e9e97dd332e2b, 0x9956e96f71581f0a]; +const ADD_0_3: [u64; 4] = + [0xc560077ab6a36adc, 0x119e8fa01d4378ed, 0x8f365b03712a49ff, 0x25ee8c6e7c092785]; +const SUB_0_3: [u64; 4] = + [0x06e1cb36353c2da2, 0xce79dc6bdfc35de0, 0x32613b9021fd4201, 0x71cf5ca4b8f58508]; +const MUL_0_3: [u64; 4] = + [0x48cfaedd21d717fb, 0xeed244113445efc2, 0x5d21d877f2d54c25, 0x36a26d8c958d0f94]; +const ADD_0_4: [u64; 4] = + [0xc975e66ed0672047, 0x1370634326cc00dc, 0xadfe5b3aa7d61c67, 0x6113f53ea209dc27]; +const SUB_0_4: [u64; 4] = + [0x02cbec421b787837, 0xcca808c8d63ad5f1, 0x13993b58eb516f99, 0x36a9f3d492f4d066]; +const MUL_0_4: [u64; 4] = + [0xd1edf8719c5683eb, 0xd3753f7d32a9aeff, 0x8142806427a58141, 0x61768084d6871c30]; +const ADD_0_5: [u64; 4] = VALS_0; +const SUB_0_5: [u64; 4] = VALS_0; +const MUL_0_5: [u64; 4] = [0, 0, 0, 0]; +const ADD_1_2: [u64; 4] = + [0xc5feef535e5d5c0d, 0x559ec58d53ba6b4d, 0xf580120168204c49, 0x8c3a2f472a710795]; +const SUB_1_2: [u64; 4] = + [0xd89afd056b074698, 0xe04a09bfc59c7b92, 0xc9a4b3d7c64967fb, 0x99f66330605118f9]; +const MUL_1_2: [u64; 4] = + [0x9d1f6e88be6ab453, 0x8b6cae940ca35981, 0xaf08588a481f31be, 0x6b35c3702ee45952]; +const ADD_1_3: [u64; 4] = + [0x667d0d0d59c1c49c, 0x766a03eed0baad7b, 0x6ec9ed5df0099768, 0x182a3532d5f38ca8]; +const SUB_1_3: [u64; 4] = + [0xa7fed0c8d85a8762, 0x334550ba933a926d, 0x11f4cdeaa0dc8f6b, 0x640b056912dfea2b]; +const MUL_1_3: [u64; 4] = + [0x54507e2e8377c6cf, 0xcebdcbaa4b5b3a52, 0x3a1d5f5fddf8ee5d, 0x45aaf54e256b2d16]; +const ADD_1_4: [u64; 4] = + [0x6a92ec0173857a07, 0x783bd791da43356a, 0x8d91ed9526b569d0, 0x534f9e02fbf4414a]; +const SUB_1_4: [u64; 4] = + [0xa3e8f1d4be96d1f7, 0x31737d1789b20a7e, 0xf32ccdb36a30bd03, 0x28e59c98ecdf3588]; +const MUL_1_4: [u64; 4] = + [0x3a77b655fc7b4bd1, 0xf083bd58c2a04360, 0xddac49a93c019759, 0x60fb6f9c9dcfe293]; +const ADD_1_5: [u64; 4] = VALS_1; +const SUB_1_5: [u64; 4] = VALS_1; +const MUL_1_5: [u64; 4] = [0, 0, 0, 0]; +const ADD_2_3: [u64; 4] = + [0x1e001e8a8602d4ab, 0x225974d2c07fd8e0, 0xe38b4416c743bcde, 0x282f29de17911d6a]; +const SUB_2_3: [u64; 4] = + [0x5f81e246049b9771, 0xdf34c19e82ffbdd2, 0x86b624a37816b4e0, 0x740ffa14547d7aed]; +const MUL_2_3: [u64; 4] = + [0x2d38daccb08e4bcc, 0xf55b78e02cab2a30, 0xec519c89c4e11532, 0x4bd2892bbbcd8f94]; +const ADD_2_4: [u64; 4] = + [0x2215fd7e9fc68a16, 0x242b4875ca0860cf, 0x0253444dfdef8f46, 0x635492ae3d91d20d]; +const SUB_2_4: [u64; 4] = + [0x5b6c0351ead7e206, 0xdd62edfb797735e3, 0x67ee246c416ae278, 0x38ea91442e7cc64b]; +const MUL_2_4: [u64; 4] = + [0xe07ebe8d62690875, 0xce2b47ba6725fafe, 0x7d1af0658e8289e1, 0x39c4f2e8ec58cacc]; +const ADD_2_5: [u64; 4] = VALS_2; +const SUB_2_5: [u64; 4] = VALS_2; +const MUL_2_5: [u64; 4] = [0, 0, 0, 0]; +const ADD_3_4: [u64; 4] = + [0x52b229bb3273494c, 0xd130017afc6a49f4, 0xba032a3b235c67d6, 0x993ff0758b0300db]; +const SUB_3_4: [u64; 4] = + [0x8c082f8e7d84a13c, 0x8a67a700abd91f08, 0x1f9e0a5966d7bb09, 0x6ed5ef0b7bedf51a]; +const MUL_3_4: [u64; 4] = + [0xe1955592a9a94d97, 0x505e6c124682c915, 0xcb9ec3ddc5ad25ac, 0xa107e6ff1d8dce50]; +const ADD_3_5: [u64; 4] = VALS_3; +const SUB_3_5: [u64; 4] = VALS_3; +const MUL_3_5: [u64; 4] = [0, 0, 0, 0]; +const ADD_4_5: [u64; 4] = VALS_4; +const SUB_4_5: [u64; 4] = VALS_4; +const MUL_4_5: [u64; 4] = [0, 0, 0, 0]; + +const NEG_0: [u64; 4] = + [0xa9fd252a21588a68, 0x1c2d449db6de3b90, 0x5d9a3f46d3efc771, 0x5e1c6352076f5375]; +const INV_0: [u64; 4] = + [0x4f5f222f13a09abf, 0xdc003eb5adc28528, 0x68c08401d6ec6be5, 0x95e72034918aefe3]; +const NEG_1: [u64; 4] = + [0x08e01f977e3a30a8, 0xb761d04f03670703, 0x7e06acec55107a07, 0x6be0ba8dad84ee52]; +const INV_1: [u64; 4] = + [0x04a5002efe23ca71, 0x5bc05734856c9eaf, 0x592970a68de0a132, 0x44715491a82b9551]; +const NEG_2: [u64; 4] = + [0x515d0e1a51f92099, 0x0b725f6b13a1db9e, 0x094556337dd65492, 0x5bdbc5e26be75d90]; +const INV_2: [u64; 4] = + [0x168949a4d8dd2336, 0x61580e6e2be323af, 0x3f401a84279c021c, 0x665b5b540a107369]; +const NEG_3: [u64; 4] = + [0x20c0e1ddbf4c6163, 0x5e6da665e13ff279, 0x5195704658697c01, 0x25f0681b1e762ec1]; +const INV_3: [u64; 4] = + [0x81f1754ef639e429, 0x355607c4866fbfd0, 0xd839243b5d9715e2, 0x90189cde76dd48f2]; +const NEG_4: [u64; 4] = + [0xacc9116c3cd1029f, 0xe8d54d668d191181, 0x71337a9fbf41370a, 0x94c657269a6423db]; +const INV_4: [u64; 4] = + [0x690a944e60c54b8c, 0xc874c6e54f35caba, 0x59490d3736d0f8b5, 0x02646e0c805fb286]; +const NEG_5: [u64; 4] = [0, 0, 0, 0]; +const INV_5: [u64; 4] = [0, 0, 0, 0]; + +fn fe(limbs: [u64; 4]) -> Bp256r1ScalarField { + Bp256r1ScalarField::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5].map(fe); + let add: Vec> = vec![ + vec![ADD_0_1, ADD_0_2, ADD_0_3, ADD_0_4, ADD_0_5], + vec![ADD_1_2, ADD_1_3, ADD_1_4, ADD_1_5], + vec![ADD_2_3, ADD_2_4, ADD_2_5], + vec![ADD_3_4, ADD_3_5], + vec![ADD_4_5], + ]; + let sub: Vec> = vec![ + vec![SUB_0_1, SUB_0_2, SUB_0_3, SUB_0_4, SUB_0_5], + vec![SUB_1_2, SUB_1_3, SUB_1_4, SUB_1_5], + vec![SUB_2_3, SUB_2_4, SUB_2_5], + vec![SUB_3_4, SUB_3_5], + vec![SUB_4_5], + ]; + let mul: Vec> = vec![ + vec![MUL_0_1, MUL_0_2, MUL_0_3, MUL_0_4, MUL_0_5], + vec![MUL_1_2, MUL_1_3, MUL_1_4, MUL_1_5], + vec![MUL_2_3, MUL_2_4, MUL_2_5], + vec![MUL_3_4, MUL_3_5], + vec![MUL_4_5], + ]; + for i in 0..6 { + for j in (i + 1)..6 { + let expected_add = fe(add[i][j - i - 1]); + let expected_sub = fe(sub[i][j - i - 1]); + let expected_mul = fe(mul[i][j - i - 1]); + assert_eq!(vals[i].add(&vals[j]), expected_add, "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), expected_add, "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), expected_sub, "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), expected_mul, "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), expected_mul, "mul is commutative ({j},{i})"); + } + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5]; + let negs = [NEG_0, NEG_1, NEG_2, NEG_3, NEG_4, NEG_5]; + let invs = [INV_0, INV_1, INV_2, INV_3, INV_4, INV_5]; + for i in 0..6 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5].map(fe); + for &v in &vals { + assert_eq!(v.add(&Bp256r1ScalarField::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&Bp256r1ScalarField::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), Bp256r1ScalarField::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), Bp256r1ScalarField::ZERO, "x + (-x) == 0"); + if v != Bp256r1ScalarField::ZERO { + assert_eq!(v.mul(&v.invert()), Bp256r1ScalarField::ONE, "x * x^-1 == 1"); + } else { + assert_eq!(v.invert(), Bp256r1ScalarField::ZERO, "0^-1 == 0 by convention"); + } + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..4 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + assert_eq!(fe(N_LIMBS), Bp256r1ScalarField::ZERO); + let n_plus_1: [u64; 4] = + [0x901e0e82974856a8, 0x8c397aa3b561a6f7, 0x3e660a909d838d71, 0xa9fb57dba1eea9bc]; + assert_eq!(fe(n_plus_1), Bp256r1ScalarField::ONE); +} + +#[test] +fn public_scalar_round_trips_and_reduces() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + assert_eq!(Bp256r1PublicScalar::from_limbs(limbs).to_limbs(), limbs); + } + assert_eq!(Bp256r1PublicScalar::from_limbs(N_LIMBS).to_limbs(), [0, 0, 0, 0]); + let n_plus_1: [u64; 4] = + [0x901e0e82974856a8, 0x8c397aa3b561a6f7, 0x3e660a909d838d71, 0xa9fb57dba1eea9bc]; + assert_eq!(Bp256r1PublicScalar::from_limbs(n_plus_1).to_limbs(), [1, 0, 0, 0]); +} + +#[test] +fn public_scalar_eq_detects_a_difference_in_any_limb() { + let base = Bp256r1PublicScalar::from_limbs(VALS_0); + assert_eq!(base, Bp256r1PublicScalar::from_limbs(VALS_0)); + for limb_idx in 0..4 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!( + base, + Bp256r1PublicScalar::from_limbs(other), + "a difference in limb {limb_idx} must be detected" + ); + } +} + +#[test] +fn secret_scalar_be_bytes_round_trip() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4] { + let secret = Bp256r1Scalar::from_limbs(limbs); + let round_tripped = Bp256r1Scalar::from_be_bytes(&secret.to_be_bytes()); + assert_eq!(round_tripped, secret); + } +} + +#[test] +fn secret_scalar_from_be_bytes_reduces_out_of_range_input() { + let mut n_bytes = [0u8; 32]; + for (i, limb) in N_LIMBS.iter().rev().enumerate() { + n_bytes[i * 8..i * 8 + 8].copy_from_slice(&limb.to_be_bytes()); + } + assert_eq!(Bp256r1Scalar::from_be_bytes(&n_bytes), Bp256r1Scalar::from_limbs([0, 0, 0, 0])); +} + +#[test] +fn scalar_field_from_secret_matches_from_limbs() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + let secret = Bp256r1Scalar::from_limbs(limbs); + assert_eq!(Bp256r1ScalarField::from_secret(&secret), fe(limbs)); + } +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property test. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0xB7A1E5B7A1E5B7A1); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != Bp256r1ScalarField::ZERO { + assert_eq!(a.mul(&a.invert()), Bp256r1ScalarField::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE000000000C); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 4], + { + let mut l = [0u64; 4]; + l[0] = 1; + l + }, + [u64::MAX; 4], + { + let mut l = [u64::MAX; 4]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `zeroize` is the signing path's way of not leaving `d`, `k` or `k^-1` legible on the stack +/// (see the method's own docs for why this type is not wrapped in `Secret` instead). Whether the +/// volatile write survives optimization cannot be observed from Rust; that the value is actually +/// cleared can be, and is what would break if the method were ever reduced to a no-op. +#[test] +fn zeroize_clears_the_value() { + let mut a = fe([1, 2, 3, 4]); + assert_ne!(a, Bp256r1ScalarField::ZERO, "precondition: the value starts non-zero"); + a.zeroize(); + assert_eq!(a, Bp256r1ScalarField::ZERO, "zeroize must leave the value at zero"); + + // and it is idempotent, so a caller scrubbing twice on overlapping paths is harmless + a.zeroize(); + assert_eq!(a, Bp256r1ScalarField::ZERO); +} + +/// `is_zero` on the scalar field, pinned for both truth values and across every limb position +/// (see the field test of the same name for why); it is the check behind `negate`'s `0 -> 0` +/// special case. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(Bp256r1ScalarField::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::bp256r1_scalar::N_LIMBS).is_zero().to_bool(), "n reduces to 0"); + assert!(!Bp256r1ScalarField::ONE.is_zero().to_bool()); + for limb_idx in 0..4 { + let mut limbs = [0u64; 4]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `n - 1`, the largest canonical scalar, is the worst case for REDC's final conditional +/// subtraction and for `add`'s carry correction, and was absent from every known-answer set +/// above (whose values are pseudorandom, so never near `n`). Expected values follow from +/// `(n-1)^2 == 1` and `(n-1) + (n-1) == n - 2` in the field, computed from `n` in Python. +#[test] +fn known_answer_at_n_minus_1() { + let n_minus_1_limbs: [u64; 4] = + [0x901e0e82974856a6, 0x8c397aa3b561a6f7, 0x3e660a909d838d71, 0xa9fb57dba1eea9bc]; + let n_minus_1 = fe(n_minus_1_limbs); + let n_minus_2 = + fe([0x901e0e82974856a5, 0x8c397aa3b561a6f7, 0x3e660a909d838d71, 0xa9fb57dba1eea9bc]); + + assert_eq!(n_minus_1.to_limbs(), n_minus_1_limbs, "Montgomery round trip of n-1"); + assert_eq!(Bp256r1PublicScalar::from_limbs(n_minus_1_limbs).to_limbs(), n_minus_1_limbs); + assert_eq!(n_minus_1.add(&n_minus_1), n_minus_2, "(n-1) + (n-1) == n-2"); + assert_eq!(n_minus_1.add(&Bp256r1ScalarField::ONE), Bp256r1ScalarField::ZERO, "(n-1) + 1 == 0"); + assert_eq!(Bp256r1ScalarField::ZERO.sub(&Bp256r1ScalarField::ONE), n_minus_1, "0 - 1 == n-1"); + assert_eq!(n_minus_1.mul(&n_minus_1), Bp256r1ScalarField::ONE, "(n-1)^2 == 1"); + assert_eq!(n_minus_1.square(), Bp256r1ScalarField::ONE, "(n-1)^2 == 1, via square"); + assert_eq!(n_minus_1.negate(), Bp256r1ScalarField::ONE, "-(n-1) == 1"); + assert_eq!(n_minus_1.invert(), n_minus_1, "(n-1)^-1 == n-1"); + assert_eq!(Bp256r1ScalarField::ONE.invert(), Bp256r1ScalarField::ONE, "1^-1 == 1"); +} + +/// The variable-time inverse on the public scalar type must agree with the constant-time +/// [`Bp256r1ScalarField::invert`] everywhere: on the known-answer values (whose inverses are pinned above +/// from Python), on `0` (both return `0` by convention), `1`, `n - 1`, and on pseudorandom +/// values -- including ones with long runs of zero bits, which are where the binary algorithm +/// takes its longest halving chains. +#[test] +fn public_scalar_invert_vartime_agrees_with_constant_time_invert() { + let n_minus_1 = { + let mut l = bouncycastle_ec::bp256r1_scalar::N_LIMBS; + l[0] -= 1; + l + }; + let mut one = [0u64; 4]; + one[0] = 1; + // Values whose low limb is exactly 1 with higher limbs set: odd, so the binary algorithm must + // not halve them, yet indistinguishable from `1` by their low limb alone. A halving test that + // looked at more than the lowest bit (or at the wrong limb) breaks the `u == x1 * a` invariant + // on exactly these and nowhere the pseudorandom inputs below would reach. + let mut low_limb_one_a = [0u64; 4]; + low_limb_one_a[0] = 1; + low_limb_one_a[1] = 1; + let mut low_limb_one_b = [0u64; 4]; + low_limb_one_b[0] = 1; + low_limb_one_b[3] = 1; + let mut low_limb_one_c = [0u64; 4]; + low_limb_one_c[0] = 1; + low_limb_one_c[1] = u64::MAX; + let mut fixed: Vec<[u64; 4]> = + vec![[0u64; 4], one, n_minus_1, low_limb_one_a, low_limb_one_b, low_limb_one_c]; + fixed.extend([VALS_0, VALS_1, VALS_2]); + for limbs in fixed { + assert_eq!( + bouncycastle_ec::bp256r1_scalar::Bp256r1PublicScalar::from_limbs(limbs) + .invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } + assert_eq!( + bouncycastle_ec::bp256r1_scalar::Bp256r1PublicScalar::from_limbs([0u64; 4]) + .invert_vartime(), + Bp256r1ScalarField::ZERO, + "0^-1 == 0 by convention, matching invert" + ); + + let mut rng = Xorshift64(0x1AC0_B5EC_0000_0004); + for i in 0..500 { + let mut limbs = rng.next_limbs(); + if i % 4 == 0 { + // Sparse values: clear the low half of each limb so `u`/`v` shed many bits per step. + for limb in limbs.iter_mut() { + *limb &= 0xffff_ffff_0000_0000; + } + } + assert_eq!( + bouncycastle_ec::bp256r1_scalar::Bp256r1PublicScalar::from_limbs(limbs) + .invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } +} diff --git a/crypto/ec/tests/bp256r1_sec1_tests.rs b/crypto/ec/tests/bp256r1_sec1_tests.rs new file mode 100644 index 000000000..19880742b --- /dev/null +++ b/crypto/ec/tests/bp256r1_sec1_tests.rs @@ -0,0 +1,134 @@ +//! Known-answer tests for SEC 1 §2.3.3/§2.3.4 point encoding and decoding, brainpoolP256r1. +//! +//! `G`'s coordinates are RFC 5639 §3.4's (already verified independently in +//! `bp256r1_point_tests.rs`); `2G`'s were computed independently in Python via the standard +//! general-`a` affine group law, specifically because it has even `y` where `G`'s is odd, so +//! between them both compressed tags (`0x02`/`0x03`) are exercised. The encodings below were +//! derived from those coordinates independently in Python via plain big-endian byte packing. + +use bouncycastle_ec::bp256r1::Bp256r1FieldElement; +use bouncycastle_ec::bp256r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp256r1_sec1::{decode, decode_point, encode_compressed, encode_uncompressed}; + +const TWO_G_X: [u64; 4] = + [0xd51a14c2ce13ea0e, 0x36ef044166699e37, 0xb55f8aa369593ac4, 0x743cf1b8b5cd4f2e]; +const TWO_G_Y: [u64; 4] = + [0x892ada097eeb7cd4, 0x38df059f69249406, 0x946fe0bb776529da, 0x36ed163337deba9c]; + +const UNCOMPRESSED_G: [u8; 65] = [ + 0x04, 0x8b, 0xd2, 0xae, 0xb9, 0xcb, 0x7e, 0x57, 0xcb, 0x2c, 0x4b, 0x48, 0x2f, 0xfc, 0x81, 0xb7, + 0xaf, 0xb9, 0xde, 0x27, 0xe1, 0xe3, 0xbd, 0x23, 0xc2, 0x3a, 0x44, 0x53, 0xbd, 0x9a, 0xce, 0x32, + 0x62, 0x54, 0x7e, 0xf8, 0x35, 0xc3, 0xda, 0xc4, 0xfd, 0x97, 0xf8, 0x46, 0x1a, 0x14, 0x61, 0x1d, + 0xc9, 0xc2, 0x77, 0x45, 0x13, 0x2d, 0xed, 0x8e, 0x54, 0x5c, 0x1d, 0x54, 0xc7, 0x2f, 0x04, 0x69, + 0x97, +]; + +const COMPRESSED_G: [u8; 33] = [ + 0x03, 0x8b, 0xd2, 0xae, 0xb9, 0xcb, 0x7e, 0x57, 0xcb, 0x2c, 0x4b, 0x48, 0x2f, 0xfc, 0x81, 0xb7, + 0xaf, 0xb9, 0xde, 0x27, 0xe1, 0xe3, 0xbd, 0x23, 0xc2, 0x3a, 0x44, 0x53, 0xbd, 0x9a, 0xce, 0x32, + 0x62, +]; + +const UNCOMPRESSED_TWO_G: [u8; 65] = [ + 0x04, 0x74, 0x3c, 0xf1, 0xb8, 0xb5, 0xcd, 0x4f, 0x2e, 0xb5, 0x5f, 0x8a, 0xa3, 0x69, 0x59, 0x3a, + 0xc4, 0x36, 0xef, 0x04, 0x41, 0x66, 0x69, 0x9e, 0x37, 0xd5, 0x1a, 0x14, 0xc2, 0xce, 0x13, 0xea, + 0x0e, 0x36, 0xed, 0x16, 0x33, 0x37, 0xde, 0xba, 0x9c, 0x94, 0x6f, 0xe0, 0xbb, 0x77, 0x65, 0x29, + 0xda, 0x38, 0xdf, 0x05, 0x9f, 0x69, 0x24, 0x94, 0x06, 0x89, 0x2a, 0xda, 0x09, 0x7e, 0xeb, 0x7c, + 0xd4, +]; + +const COMPRESSED_TWO_G: [u8; 33] = [ + 0x02, 0x74, 0x3c, 0xf1, 0xb8, 0xb5, 0xcd, 0x4f, 0x2e, 0xb5, 0x5f, 0x8a, 0xa3, 0x69, 0x59, 0x3a, + 0xc4, 0x36, 0xef, 0x04, 0x41, 0x66, 0x69, 0x9e, 0x37, 0xd5, 0x1a, 0x14, 0xc2, 0xce, 0x13, 0xea, + 0x0e, +]; + +fn fe(limbs: [u64; 4]) -> Bp256r1FieldElement { + Bp256r1FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_encode_uncompressed() { + assert_eq!(encode_uncompressed(&fe(G_X_LIMBS), &fe(G_Y_LIMBS)), UNCOMPRESSED_G); + assert_eq!(encode_uncompressed(&fe(TWO_G_X), &fe(TWO_G_Y)), UNCOMPRESSED_TWO_G); +} + +#[test] +fn known_answer_encode_compressed() { + assert_eq!( + encode_compressed(&fe(G_X_LIMBS), &fe(G_Y_LIMBS)), + COMPRESSED_G, + "G has odd y -> tag 0x03" + ); + assert_eq!( + encode_compressed(&fe(TWO_G_X), &fe(TWO_G_Y)), + COMPRESSED_TWO_G, + "2G has even y -> tag 0x02" + ); +} + +#[test] +fn known_answer_decode_uncompressed() { + let (x, y) = decode(&UNCOMPRESSED_G).expect("valid uncompressed point"); + assert_eq!(x, fe(G_X_LIMBS)); + assert_eq!(y, fe(G_Y_LIMBS)); +} + +#[test] +fn known_answer_decode_compressed() { + let (x, y) = decode(&COMPRESSED_G).expect("valid compressed point, tag 0x03"); + assert_eq!(x, fe(G_X_LIMBS)); + assert_eq!(y, fe(G_Y_LIMBS)); + + let (x2, y2) = decode(&COMPRESSED_TWO_G).expect("valid compressed point, tag 0x02"); + assert_eq!(x2, fe(TWO_G_X)); + assert_eq!(y2, fe(TWO_G_Y)); +} + +#[test] +fn round_trip_uncompressed_and_compressed() { + for (x, y) in [(G_X_LIMBS, G_Y_LIMBS), (TWO_G_X, TWO_G_Y)] { + let uncompressed = encode_uncompressed(&fe(x), &fe(y)); + assert_eq!(decode(&uncompressed), Some((fe(x), fe(y)))); + + let compressed = encode_compressed(&fe(x), &fe(y)); + assert_eq!(decode(&compressed), Some((fe(x), fe(y)))); + + assert!(decode_point(&uncompressed).unwrap().to_affine().is_some()); + } +} + +#[test] +fn decode_rejects_malformed_input() { + assert_eq!(decode(&[]), None); + assert_eq!(decode(&[0x04; 64]), None); + assert_eq!(decode(&[0x04; 66]), None); + assert_eq!(decode(&[0x02; 16]), None); + + let mut bad_tag_65 = UNCOMPRESSED_G; + bad_tag_65[0] = 0x02; + assert_eq!(decode(&bad_tag_65), None); + + let mut bad_tag_33 = COMPRESSED_G; + bad_tag_33[0] = 0x04; + assert_eq!(decode(&bad_tag_33), None); + + assert_eq!(decode(&[0x00]), None); + + let mut x_too_big = COMPRESSED_G; + x_too_big[1..33].fill(0xff); + assert_eq!(decode(&x_too_big), None); + + let mut no_root = COMPRESSED_G; + no_root[1..33].fill(0); + no_root[32] = 4; // x = 4, confirmed (in the Python derivation) to have no square root + assert_eq!(decode(&no_root), None); + + let mut not_on_curve = UNCOMPRESSED_G; + not_on_curve[64] ^= 0x01; + assert_eq!(decode(¬_on_curve), None); + + let mut y_too_big = UNCOMPRESSED_G; + y_too_big[33..65].fill(0xff); + assert_eq!(decode(&y_too_big), None); +} diff --git a/crypto/ec/tests/bp256r1_wnaf_tests.rs b/crypto/ec/tests/bp256r1_wnaf_tests.rs new file mode 100644 index 000000000..46cb328c5 --- /dev/null +++ b/crypto/ec/tests/bp256r1_wnaf_tests.rs @@ -0,0 +1,148 @@ +//! Known-answer tests for [`shamir_multiply`] (`[u]G + [v]Q`), brainpoolP256r1. +//! +//! `Q` below is `d*G` for a fixed test-only `d` (not a real key). Expected results were computed +//! independently in Python from the affine group law (general `a`), the same way as +//! `bp256r1_comb_tests.rs`, with `random.seed(808384)`. + +use bouncycastle_ec::bp256r1::Bp256r1FieldElement; +use bouncycastle_ec::bp256r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp256r1_point::Bp256r1JacobianPoint; +use bouncycastle_ec::bp256r1_scalar::{Bp256r1PublicScalar, Bp256r1Scalar}; +use bouncycastle_ec::bp256r1_wnaf::shamir_multiply; + +const Q_X: [u64; 4] = + [0xfec96d58db262cee, 0x2988091aa5352214, 0xf504fa7c525258cb, 0x168b3be33d1862b3]; +const Q_Y: [u64; 4] = + [0xec9068e7d3dc6b66, 0xe1dc7803f8f01613, 0xa4196062cbcc1858, 0x22e28e07075599e5]; + +fn q() -> Bp256r1JacobianPoint { + Bp256r1JacobianPoint::from_affine( + Bp256r1FieldElement::from_limbs(Q_X), + Bp256r1FieldElement::from_limbs(Q_Y), + ) +} + +fn scalar(limbs: [u64; 4]) -> Bp256r1PublicScalar { + Bp256r1PublicScalar::from_limbs(limbs) +} + +fn assert_result_is(u: [u64; 4], v: [u64; 4], expected_x: [u64; 4], expected_y: [u64; 4]) { + let result = shamir_multiply(&scalar(u), &scalar(v), &q()); + let (rx, ry) = result.to_affine().expect("expected a non-infinity result"); + assert_eq!(rx, Bp256r1FieldElement::from_limbs(expected_x), "x mismatch for u={u:x?} v={v:x?}"); + assert_eq!(ry, Bp256r1FieldElement::from_limbs(expected_y), "y mismatch for u={u:x?} v={v:x?}"); +} + +#[test] +fn known_answer_large_u_v() { + let u: [u64; 4] = + [0x48403aad19a04ae7, 0x74cc2df91a4a3ac6, 0xa73e462c3bc5bf0c, 0x838e541242a28502]; + let v: [u64; 4] = + [0xab460788b359c7e8, 0xc53f364b2831dc1d, 0x88aebe98b4707af4, 0x0cb0e573234caea1]; + assert_result_is( + u, + v, + [0xbb838ffd0371790a, 0x4a6d6d80383ef9b4, 0x977db6b0671f717f, 0x690f712b194fca7f], + [0x75521b078d47c966, 0x70d10ec465a9c3b6, 0x28640fe13c1fa2d3, 0x43df13a6d54004c1], + ); +} + +#[test] +fn edge_case_v_is_zero_reduces_to_u_times_g() { + assert_result_is( + [5, 0, 0, 0], + [0, 0, 0, 0], + [0x32f95f7c85fe101d, 0x7cf41589c0d8c3fb, 0xa5f863e8b69fc147, 0x855433a3a4c8e334], + [0xef9e224a5fd8814c, 0x097082129591c88b, 0xd7e172e40350d911, 0xa50c95efc2ad06c4], + ); +} + +#[test] +fn edge_case_u_is_zero_reduces_to_v_times_q() { + assert_result_is( + [0, 0, 0, 0], + [5, 0, 0, 0], + [0xfbac247234e00fb4, 0x14454623c3fd1cfe, 0x687d606b92c96503, 0x453d28b5a120a7c9], + [0x7c759dd686bb3b84, 0x06fcf1dccf3875c7, 0xe2180f511dff15d7, 0x2e0931f528334076], + ); +} + +#[test] +fn edge_case_both_zero_is_infinity() { + let result = shamir_multiply(&scalar([0; 4]), &scalar([0; 4]), &q()); + assert!(result.to_affine().is_none()); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn cross_checked_against_the_comb_multiplier_over_many_pseudorandom_scalars() { + // shamir_multiply(k, 0, anything) must equal comb_multiply_base_point(k): two independently + // implemented multipliers (fixed-base comb vs. interleaved wNAF) computing the same [k]G. + let mut rng = Xorshift64(0xE7C558A6A6E7F1DE); + for _ in 0..500 { + let k_limbs = rng.next_limbs(); + let via_comb = comb_multiply_base_point(&Bp256r1Scalar::from_limbs(k_limbs)); + let via_wnaf = + shamir_multiply(&Bp256r1PublicScalar::from_limbs(k_limbs), &scalar([0; 4]), &q()); + assert_eq!(via_comb.to_affine(), via_wnaf.to_affine(), "k = {k_limbs:x?}"); + } +} + +/// Every other test in this file uses one fixed `Q = dG`. The three `Q` values here each take a +/// path that one cannot: `Q = infinity` makes every `[v]Q` digit an addition of the identity, +/// `Q = G` makes the two precomputed odd-multiple tables identical (so the interleaved +/// additions hit the same-point case of `add_vartime`), and `Q = -G` makes them negatives of +/// each other (the opposite-point case, and with `u == v` a final result of infinity). Expected +/// values come from the fixed-base comb multiplier on the combined scalar, which is pinned by +/// its own known-answer tests. +#[test] +fn edge_case_q_is_infinity_g_or_minus_g() { + let g = Bp256r1JacobianPoint::from_affine( + Bp256r1FieldElement::from_limbs(bouncycastle_ec::bp256r1_domain::G_X_LIMBS), + Bp256r1FieldElement::from_limbs(bouncycastle_ec::bp256r1_domain::G_Y_LIMBS), + ); + let mut u = [0u64; 4]; + u[0] = 12345; + let mut v = [0u64; 4]; + v[0] = 6789; + let mut u_plus_v = [0u64; 4]; + u_plus_v[0] = 19134; + let mut u_minus_v = [0u64; 4]; + u_minus_v[0] = 5556; + let via_comb = + |k: [u64; 4]| comb_multiply_base_point(&Bp256r1Scalar::from_limbs(k)).to_affine(); + + // [u]G + [v]infinity == [u]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &Bp256r1JacobianPoint::INFINITY).to_affine(), + via_comb(u) + ); + // [u]G + [v]G == [u+v]G + assert_eq!(shamir_multiply(&scalar(u), &scalar(v), &g).to_affine(), via_comb(u_plus_v)); + // [u]G + [v](-G) == [u-v]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &g.negate()).to_affine(), + via_comb(u_minus_v) + ); + // [u]G + [u](-G) == infinity + assert!(shamir_multiply(&scalar(u), &scalar(u), &g.negate()).is_infinity().to_bool()); + // [0]G + [v]G == [v]G: the G table is never consulted, only Q's + assert_eq!(shamir_multiply(&scalar([0u64; 4]), &scalar(v), &g).to_affine(), via_comb(v)); +} diff --git a/crypto/ec/tests/bp384r1_comb_tests.rs b/crypto/ec/tests/bp384r1_comb_tests.rs new file mode 100644 index 000000000..80d60e799 --- /dev/null +++ b/crypto/ec/tests/bp384r1_comb_tests.rs @@ -0,0 +1,95 @@ +//! Known-answer tests for [`comb_multiply_base_point`] (`[k]G`), brainpoolP384r1. +//! +//! `k = 2, 3` reuse the values verified independently in `bp384r1_point_tests.rs`; `k` for +//! `known_answer_large_k` and `n-1` (where `[k]G = -G`) were computed the same way (Python affine +//! group law, general `a`), `random.seed(628384)` for the former. + +use bouncycastle_ec::bp384r1::Bp384r1FieldElement; +use bouncycastle_ec::bp384r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp384r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp384r1_scalar::Bp384r1Scalar; + +fn scalar(limbs: [u64; 6]) -> Bp384r1Scalar { + Bp384r1Scalar::from_limbs(limbs) +} + +fn assert_result_is(k_limbs: [u64; 6], x: [u64; 6], y: [u64; 6]) { + let result = comb_multiply_base_point(&scalar(k_limbs)); + let (rx, ry) = result.to_affine().expect("[k]G must not be infinity for k in (0, n)"); + assert_eq!(rx, Bp384r1FieldElement::from_limbs(x), "x mismatch for k = {k_limbs:x?}"); + assert_eq!(ry, Bp384r1FieldElement::from_limbs(y), "y mismatch for k = {k_limbs:x?}"); +} + +#[test] +fn known_answer_small_k() { + assert_result_is([1, 0, 0, 0, 0, 0], G_X_LIMBS, G_Y_LIMBS); + assert_result_is( + [2, 0, 0, 0, 0, 0], + [ + 0xb427b58df9d59fca, 0x7c4ba74a09b553eb, 0xec2f80c4e0f70df8, 0x0ad520b3eb6be4d6, + 0xb95c3495d7b4fd59, 0x2282bc382a2f4dfc, + ], + [ + 0x8f1b29502b6e1d30, 0xb4b4ea69ab64fc28, 0x4d89dd051728fc3e, 0xce9bedbc170921ce, + 0x5768d14a24f37a57, 0x0edda83773ac6873, + ], + ); + assert_result_is( + [3, 0, 0, 0, 0, 0], + [ + 0x7d7cc85b3035f11f, 0x3c11ef6596a3b889, 0x3ee1b6fcc7463bbe, 0x3df581348c6949f8, + 0x3b17452b6a27ebf5, 0x7b63205bf00ddae7, + ], + [ + 0x81d7129d48772eb3, 0x73c0edaea3b8f593, 0x7b2bd39462363e03, 0x7b2eb481ead16a5c, + 0x5521a326bc02baaf, 0x761d3a4a5f809377, + ], + ); +} + +#[test] +fn known_answer_large_k() { + let k: [u64; 6] = [ + 0xe44e54d2e3f53e11, 0xa478d04d639ee021, 0xc7175d7fc6bd2622, 0x5255ca4f4e4bb63f, + 0xd4eb1f80a5dce29a, 0x4750f0f05ef0a726, + ]; + assert_result_is( + k, + [ + 0x4148e10e2b92329f, 0x6015335e1dd0d619, 0x5ac90c58222c91d7, 0x70e2ac39059dcb18, + 0x0428ae9a89c8c305, 0x18d71950a766bb77, + ], + [ + 0x10a6b41526614628, 0xff52de7651315dd8, 0x2d53719a34fe2421, 0x44cd86eca5032cbe, + 0x3c1bacd9abdfb6ed, 0x60157c879854ba88, + ], + ); +} + +#[test] +fn edge_case_k_equals_n_minus_1() { + // (n-1)*G == -G: same x as G, negated y. + let n_minus_1: [u64; 6] = [ + 0x3b883202e9046564, 0xcf3ab6af6b7fc310, 0x1f166e6cac0425a7, 0x152f7109ed5456b3, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ]; + assert_result_is( + n_minus_1, + G_X_LIMBS, + [ + 0x44c4fcd20acb993e, 0x9e8d6108188b9960, 0x3115d4c98625e7fb, 0xb27865dfee67fe4f, + 0xb2ab83efbb166c8c, 0x01fb010d823eaa83, + ], + ); +} + +/// `k = 0` is the one scalar for which `[k]G` is the identity: every comb digit is `0`, so every +/// round selects table entry `0` (the infinity sentinel) and the accumulator never leaves +/// `INFINITY`. Nothing in the signing path can produce it (`k` is in `[1, n-1]` by construction), +/// which is exactly why it needs pinning here rather than being left to chance. +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let result = comb_multiply_base_point(&scalar([0u64; 6])); + assert!(result.is_infinity().to_bool()); + assert!(result.to_affine().is_none()); +} diff --git a/crypto/ec/tests/bp384r1_extra_bits_tests.rs b/crypto/ec/tests/bp384r1_extra_bits_tests.rs new file mode 100644 index 000000000..0784ef3e8 --- /dev/null +++ b/crypto/ec/tests/bp384r1_extra_bits_tests.rs @@ -0,0 +1,143 @@ +//! Known-answer tests for [`reduce_wide_bits_mod_n_minus_1`] (FIPS 186-5 Appendix A.4.1 steps +//! 3-5): `x = bits2int(X)`, `x = x mod (n-1)`, `x = x + 1`. Expected values computed independently +//! in Python via `(int.from_bytes(bytes, 'big') % (n - 1)) + 1` with `n` from RFC 5639 §3.4, not +//! derived from this crate's own reduction code -- see `extra_bits_bp256r1_tests.rs`'s docs for why +//! this independent-reference pinning matters (a real test gap `cargo mutants` found on that +//! curve's analogous constant and reduction). + +use bouncycastle_ec::bp384r1_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_hex::decode as hex_decode; + +fn bytes48(hex: &str) -> [u8; 48] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +#[test] +fn known_answer_repeating_pattern() { + let input: Vec = [0x01u8, 0x02, 0x03, 0x04].iter().cloned().cycle().take(56).collect(); + let expected = bytes48( + "601f5e94713f3101088d9b91ddd7101eef885d40f508b354b55c1848db5b1a0b408636d749e783b059eda819efd1cd29", + ); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_ff() { + let input = [0xffu8; 56]; + let expected = bytes48( + "8c411bc9d770ff441c959a14f5397a62dbddfd1e9e782eab85d0816fe833a4b180553a48c19d96aa7284dfef4cf3c820", + ); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_zero_yields_one() { + // bits2int(0) = 0; 0 mod (n-1) = 0; +1 = 1 -- the smallest value Appendix A.4.1 can produce. + let input = [0u8; 56]; + let mut expected = [0u8; 48]; + expected[47] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +/// The two boundaries of Appendix A.4.1's own arithmetic: `n - 1` is the modulus of step 4, so it +/// reduces to `0` and step 5 makes it `1`; `n - 2` is the largest residue, so it comes out as +/// `n - 1`, the top of the `[1, n-1]` output interval. Both inputs are left-padded to the DRBG +/// output width this module is fed with. +#[test] +fn known_answer_at_the_n_minus_1_boundary() { + let n = bouncycastle_ec::bp384r1_sec1::be_bytes_from_limbs( + &bouncycastle_ec::bp384r1_scalar::N_LIMBS, + ); + let mut n_minus_1 = n; + n_minus_1[47] -= 1; // n is odd, so neither of these subtractions borrows + let mut n_minus_2 = n; + n_minus_2[47] -= 2; + + let mut input = [0u8; 56]; + input[8..].copy_from_slice(&n_minus_1); + let mut one = [0u8; 48]; + one[47] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), one, "n - 1 -> 1"); + + input[8..].copy_from_slice(&n_minus_2); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), n_minus_1, "n - 2 -> n - 1"); +} + +/// The bit-serial algorithm this module implemented before switching to Barrett reduction, kept +/// here as an independent reference: Horner's rule over the input bits, doubling the running +/// remainder and subtracting `n - 1` when it is exceeded, with the carry out of the top limb folded +/// back in as `2^384 mod (n-1) = 2^384 - (n-1)`. Slow, obviously correct, and sharing no +/// code with the Barrett path beyond `nat`'s add and sub. +fn reference_bit_serial(bytes: &[u8]) -> [u8; 48] { + use bouncycastle_ec::nat; + let n = bouncycastle_ec::bp384r1_scalar::N_LIMBS; + let mut m = n; + m[0] -= 1; // n is odd + let two_pow_mod_m = nat::sub(&[0u64; 6], &m).0; // 2^384 - m, wrapping + let mut acc = [0u64; 6]; + for &byte in bytes { + for bit_idx in (0..8).rev() { + let (mut doubled, carry) = nat::add(&acc, &acc); + doubled[0] |= ((byte >> bit_idx) & 1) as u64; + if carry == 1 { + doubled = nat::add(&doubled, &two_pow_mod_m).0; + } + let (reduced, borrow) = nat::sub(&doubled, &m); + acc = if borrow == 1 { doubled } else { reduced }; + } + } + let plus_one = nat::add(&acc, &[1u64, 0, 0, 0, 0, 0]).0; + bouncycastle_ec::bp384r1_sec1::be_bytes_from_limbs(&plus_one) +} + +/// xorshift64*, fixed seed, as in the `ec` crate's property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } +} + +/// Barrett against the bit-serial reference over pseudorandom inputs at every width from one byte +/// up to the 95-byte maximum the function accepts, biased towards the widths actually drawn +/// (56 bytes) and towards values with long runs of set bits, where Barrett's quotient estimate is +/// furthest off and the most conditional subtractions are needed. +#[test] +fn agrees_with_the_bit_serial_reference_over_many_pseudorandom_inputs() { + let mut rng = Xorshift64(0xBA77E770_0000_0006); + for i in 0..3000 { + let len = match i % 4 { + 0 => 56, + 1 => 95, + _ => 1 + (rng.next_u64() as usize % 95), + }; + let mut bytes = vec![0u8; len]; + for chunk in bytes.chunks_mut(8) { + let word = rng.next_u64().to_be_bytes(); + chunk.copy_from_slice(&word[..chunk.len()]); + } + if i % 3 == 0 { + for b in bytes.iter_mut().take(len / 2) { + *b = 0xff; + } + } + assert_eq!( + reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), + reference_bit_serial(&bytes), + "len = {len}, bytes = {bytes:02x?}" + ); + } +} + +#[test] +fn maximum_width_all_ones_input() { + // 2^760 - 1, the largest input accepted: Barrett's precondition holds with room to spare. + let bytes = [0xffu8; 95]; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), reference_bit_serial(&bytes)); +} diff --git a/crypto/ec/tests/bp384r1_field_tests.rs b/crypto/ec/tests/bp384r1_field_tests.rs new file mode 100644 index 000000000..ff090fb8e --- /dev/null +++ b/crypto/ec/tests/bp384r1_field_tests.rs @@ -0,0 +1,425 @@ +//! Known-answer tests for [`Bp384r1FieldElement`] arithmetic. Expected values computed +//! independently in Python: +//! +//! ```text +//! p = 0x8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B412B1DA197FB71123ACD3A729901D1A71874700133107EC53 +//! random.seed(384256) +//! vals = [random.randrange(1, p) for _ in range(3)] +//! ``` + +use bouncycastle_ec::bp384r1::Bp384r1FieldElement; + +const VALS_0: [u64; 6] = [ + 0xe90f0d68176b0cc9, 0x271f0b47d6dacd22, 0xe4561568c8e4b445, 0xde8c273c4ad5adc5, + 0xc3b58c6c32d5e314, 0x560cf7adaef2d174, +]; +const VALS_1: [u64; 6] = [ + 0x931520ab505d451d, 0x9bcdf724ffd8834a, 0x65529eb4084ada66, 0xad9b03fc815ca29f, + 0x4fe04118812251d0, 0x803c39b51164dc37, +]; +const VALS_2: [u64; 6] = [ + 0x0c44eade679d0641, 0x5fb85ed8258f72e6, 0x8919a88f010cdfa3, 0x8d4a0c8146d4495f, + 0x9d112be8cf6abfea, 0x577a293dc1e0b7cb, +]; + +const ADD_0_1: [u64; 6] = [ + 0xf4dd2e0036c06593, 0x16195b43469635fb, 0x36f6da0351787d88, 0x76f7ba2ededdf9b1, + 0x04385e066311f306, 0x499012e01d1f4084, +]; +const SUB_0_1: [u64; 6] = [ + 0xdd40eccff815b3ff, 0x3824bb4c671f6449, 0x91b550ce4050eb02, 0x46209449b6cd61da, + 0x8332bad20299d323, 0x6289dc7b40c66265, +]; +const MUL_0_1: [u64; 6] = [ + 0x16ab4196494bc886, 0xf9ffcf0c65de7c1a, 0x2efcf0c1d784d40d, 0xca93391ba1bfaa78, + 0xe850cc1f9e4abb0e, 0x5454c5b74eaab75b, +]; +const ADD_0_2: [u64; 6] = [ + 0x6e0cf8334e0026b7, 0xda03c2f66c4d2597, 0x5abde3de4a3a82c4, 0x56a6c2b3a455a071, + 0x516948d6b15a6120, 0x20ce0268cd9b1c18, +]; +const SUB_0_2: [u64; 6] = [ + 0x6411229ce0d5f2db, 0x743a5399416874ae, 0x6dee46f3478ee5c5, 0x66718bc4f155bb1a, + 0x3601d001b4516509, 0x8b4becf2904a86d1, +]; +const MUL_0_2: [u64; 6] = [ + 0xd55eb22ceb7b3edd, 0xc04e2d3e03f9cde5, 0x3e65bce2a2be0732, 0x889698f8c7184a34, + 0x5609874cc0afe05c, 0x713a84d1029c4523, +]; +const ADD_1_2: [u64; 6] = [ + 0x18130b7686f25f0b, 0x4eb2aed3954adbbf, 0xdbba6d2989a0a8e6, 0x25b59f73dadc954a, + 0xdd93fd82ffa6cfdc, 0x4afd4470300d26da, +]; +const SUB_1_2: [u64; 6] = [ + 0x86d035cce8c03edc, 0x3c15984cda491064, 0xdc38f625073dfac3, 0x2050f77b3a88593f, + 0xb2cf152fb1b791e6, 0x28c210774f84246b, +]; +const MUL_1_2: [u64; 6] = [ + 0x48159ae5c970da1d, 0x2304b7b61c54409f, 0xcc5d58ed1b043712, 0xe1342527d8b63815, + 0x9f40259285258008, 0x6a2d27fb82ae65c2, +]; + +const NEG_0: [u64; 6] = [ + 0x9e37f2ab199cdf8a, 0x85b49be1b9424d4e, 0x2e5bc4b0b6d25cde, 0x36a349cda27ea8ee, + 0x4ba7e3121e105eca, 0x36ac26d4f4459bb3, +]; +const INV_0: [u64; 6] = [ + 0x5497899b46d8de80, 0x502b8865ff2d58be, 0x9c5bf2402a1b41ec, 0x51fd2c0170cb44d5, + 0x5e96ca30a78ff76a, 0x2ba42d9c14320fd7, +]; +const NEG_1: [u64; 6] = [ + 0xf431df67e0aaa736, 0x1105b00490449726, 0xad5f3b65776c36bd, 0x67946d0d6bf7b414, + 0xbf7d2e65cfc3f00e, 0x0c7ce4cd91d390f0, +]; +const INV_1: [u64; 6] = [ + 0xc7e57cb0e8d1c7c7, 0x7abbf2c650b96c6e, 0xc3fe516f3ac8d0ab, 0xd6d53abfd2a65751, + 0x10f52f8b187fb324, 0x0ebfbe830ce54e9f, +]; +const NEG_2: [u64; 6] = [ + 0x7b021534c96ae612, 0x4d1b48516a8da78b, 0x8998318a7eaa3180, 0x87e56488a6800d54, + 0x724c4395817b81f4, 0x353ef544e157b55c, +]; +const INV_2: [u64; 6] = [ + 0xfe45ef3f26ff6712, 0x2e87f5087bf83fc7, 0x07f45274b036d768, 0xdf7d46d6f954134b, + 0x67e4cd7fbae2e29e, 0x64458f53448d9fef, +]; + +fn fe(limbs: [u64; 6]) -> Bp384r1FieldElement { + Bp384r1FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + let pairs: [(usize, usize, [u64; 6], [u64; 6], [u64; 6]); 3] = [ + (0, 1, ADD_0_1, SUB_0_1, MUL_0_1), + (0, 2, ADD_0_2, SUB_0_2, MUL_0_2), + (1, 2, ADD_1_2, SUB_1_2, MUL_1_2), + ]; + for (i, j, a, s, m) in pairs { + assert_eq!(vals[i].add(&vals[j]), fe(a), "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), fe(a), "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), fe(s), "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), fe(m), "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), fe(m), "mul is commutative ({j},{i})"); + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2]; + let negs = [NEG_0, NEG_1, NEG_2]; + let invs = [INV_0, INV_1, INV_2]; + for i in 0..3 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + for &v in &vals { + assert_eq!(v.add(&Bp384r1FieldElement::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&Bp384r1FieldElement::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), Bp384r1FieldElement::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), Bp384r1FieldElement::ZERO, "x + (-x) == 0"); + assert_eq!(v.mul(&v.invert()), Bp384r1FieldElement::ONE, "x * x^-1 == 1"); + } + assert_eq!( + Bp384r1FieldElement::ZERO.invert(), + Bp384r1FieldElement::ZERO, + "0^-1 == 0 by convention" + ); +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..6 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + use bouncycastle_ec::bp384r1::P_LIMBS; + // p itself must reduce to 0, and p+1 to 1. + assert_eq!(fe(P_LIMBS), Bp384r1FieldElement::ZERO); + assert_eq!( + fe([ + 0x874700133107ec54, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28 + ]), + Bp384r1FieldElement::ONE + ); + + // The maximum limb pattern, 2^384 - 1, is the widest input the single conditional + // subtraction has to cover (it is < 2p, since p > 2^383); expected value is + // 2^384 - 1 - p, computed in Python. + assert_eq!( + fe([u64::MAX; 6]), + fe([ + 0x78b8ffeccef813ac, 0x532c58d66fe2e58e, 0xed4e25e68048eedc, 0xead08ef612aba94b, + 0xf0a29081af19be20, 0x7346e17d5cc792d7 + ]) + ); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the P-256 field test's property check. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 6] { + [ + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + ] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0xC384256C384256C3); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != Bp384r1FieldElement::ZERO { + assert_eq!(a.mul(&a.invert()), Bp384r1FieldElement::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE0000000006); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 6], + { + let mut l = [0u64; 6]; + l[0] = 1; + l + }, + [u64::MAX; 6], + { + let mut l = [u64::MAX; 6]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `is_zero` is the mask every exceptional-case select in this crate's point arithmetic keys +/// off (infinity detection, the `H == 0` / `R == 0` same-point and opposite-point cases), and +/// otherwise exercised only through those selects. Both truth values are pinned here, with +/// the nonzero side walked across every limb position so a mask that inspected only some limbs +/// would be caught. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(Bp384r1FieldElement::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::bp384r1::P_LIMBS).is_zero().to_bool(), "p reduces to 0"); + assert!(!Bp384r1FieldElement::ONE.is_zero().to_bool()); + for limb_idx in 0..6 { + let mut limbs = [0u64; 6]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `add`'s carry-out correction and `sub`'s borrow correction, pinned at the operands that force +/// them: `(p-1) + (p-1)` is the largest sum two canonical elements can form, and `0 - 1` the +/// smallest difference. The expected values are `p - 2` and `p - 1` themselves (computed from +/// `p` in Python), so this is a check of the wrap-around handling, not of the digits of `p`. +#[test] +fn known_answer_add_carry_and_sub_borrow_at_the_extremes() { + let p_minus_1 = fe([ + 0x874700133107ec52, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ]); + let p_minus_2 = fe([ + 0x874700133107ec51, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ]); + let two = fe([ + 0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ]); + + assert_eq!(p_minus_1.add(&p_minus_1), p_minus_2, "(p-1) + (p-1) == p-2"); + assert_eq!( + p_minus_1.add(&Bp384r1FieldElement::ONE), + Bp384r1FieldElement::ZERO, + "(p-1) + 1 == 0" + ); + assert_eq!(Bp384r1FieldElement::ZERO.sub(&Bp384r1FieldElement::ONE), p_minus_1, "0 - 1 == p-1"); + assert_eq!(Bp384r1FieldElement::ONE.sub(&p_minus_1), two, "1 - (p-1) == 2"); + assert_eq!(p_minus_1.negate(), Bp384r1FieldElement::ONE, "-(p-1) == 1"); + assert_eq!(Bp384r1FieldElement::ONE.invert(), Bp384r1FieldElement::ONE, "1^-1 == 1"); + assert_eq!(p_minus_1.invert(), p_minus_1, "(p-1)^-1 == p-1, since (p-1)^2 == 1"); +} + +/// Products at the reduction's extremes rather than in the middle of its range, the same class +/// of check `p256_field_tests.rs`/`p384_field_tests.rs`/`sm2_field_tests.rs` already carry and +/// this curve did not: `(p-1)^2` and `(p-1)(p-2)` (the largest products two canonical elements +/// can form), operands within `2^64` of `p`, and operands with the top bit set. Expected values +/// are Python's `(a * b) % p`, computed independently of this crate's arithmetic +/// (`random.seed(20260918)` for the pseudorandom operands). +#[test] +fn known_answer_mul_at_the_reduction_bounds() { + let cases: [([u64; 6], [u64; 6], [u64; 6]); 7] = [ + // (p-1)^2 == 1 + ( + [ + 0x874700133107ec52, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ], + [ + 0x874700133107ec52, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ], + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ], + ), + // (p-1)(p-2) == 2 + ( + [ + 0x874700133107ec52, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ], + [ + 0x874700133107ec51, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ], + [ + 0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ], + ), + // (p-1) * 2^(bits-1) == p - 2^(bits-1) + ( + [ + 0x874700133107ec52, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ], + [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x8000000000000000, + ], + [ + 0x874700133107ec53, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x0cb91e82a3386d28, + ], + ), + // both operands within 2^64 of p + ( + [ + 0x41734e2063972dc4, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ], + [ + 0x53d67b230f517356, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ], + [ + 0xa43ea609ca695b53, 0x0e07dd53e0916c49, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ], + ), + // both operands within 2^64 of p + ( + [ + 0xa120c310bcab44b9, 0xacd3a729901d1a70, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ], + [ + 0x2eb5aa0d68c92bcc, 0xacd3a729901d1a71, 0x12b1da197fb71123, 0x152f7109ed5456b4, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ], + [ + 0x01c396f3c1dbe236, 0x4f9fcdf771a8103e, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ], + ), + // both operands with the top bit set + ( + [ + 0xb78d4e3090f25903, 0x2da51dedb08ec5a1, 0xb8a6ea74a7c581c8, 0x5742eeaecb59f139, + 0xe4c3431f5094ea0e, 0x85db817f5b396b49, + ], + [ + 0x3a52a4d6d9807e5a, 0x4c48dc5d2adcd97a, 0x7d4fb60dee8ec304, 0xc790d6e7f93090cf, + 0xb5b837d8b6429198, 0x85aa0af84700cfef, + ], + [ + 0xa067d8975b2d7ef5, 0xd80119585f9da4f2, 0x3ae081d0c47a47b5, 0x878b8c71e30ac453, + 0xd4b5fb7484dd0e97, 0x7f1a5c3557b795ae, + ], + ), + // both operands with the top bit set + ( + [ + 0x1c97f452395cb7a6, 0x6504873413c160e0, 0x91afa1f9d44c827d, 0x123d60c8716c9e99, + 0xd91f628cef749847, 0x89b56ac867a46af4, + ], + [ + 0x04ab39f31042043b, 0x0b821eebd9dabf0b, 0x41b407aecf1b94ee, 0x8cb76090d0550b48, + 0xb91fb5f70b7c0b1b, 0x876b787a80283170, + ], + [ + 0x7ab7fbbeb1509215, 0x75311da4f1fe44a5, 0x098f458a63230a4d, 0x4d2ffbacbaf47fe0, + 0x24c91d4cd85aa336, 0x650cf0fa6cb8579a, + ], + ), + ]; + for (a, b, expected) in cases { + assert_eq!(fe(a).mul(&fe(b)), fe(expected), "a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(b).mul(&fe(a)), fe(expected), "commuted: a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(a).square(), fe(a).mul(&fe(a)), "square at the same extreme: a = {a:x?}"); + } +} diff --git a/crypto/ec/tests/bp384r1_mul_tests.rs b/crypto/ec/tests/bp384r1_mul_tests.rs new file mode 100644 index 000000000..e3cc0ff9c --- /dev/null +++ b/crypto/ec/tests/bp384r1_mul_tests.rs @@ -0,0 +1,172 @@ +//! Behavioural tests for [`multiply_point`] (`[k]Q`, constant time in `k`): cross-checked against +//! the fixed-base comb for `Q = G`, against the variable-time wNAF multiplier for a random `Q`, +//! and against `[a k mod n]G` for `Q = [a]G` (three multipliers of independent design computing +//! the same point), plus the edge cases a windowed multiplier can get wrong: `k = 0`, `k = 1`, +//! `k = n-1`, a window of all ones, `Q` the identity, `Q` negated, and `Q` given in a +//! non-canonical-`Z` Jacobian representation. + +use bouncycastle_ec::bp384r1::Bp384r1FieldElement; +use bouncycastle_ec::bp384r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp384r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp384r1_mul::multiply_point; +use bouncycastle_ec::bp384r1_point::Bp384r1JacobianPoint; +use bouncycastle_ec::bp384r1_scalar::{ + Bp384r1PublicScalar, Bp384r1Scalar, Bp384r1ScalarField, N_LIMBS, +}; +use bouncycastle_ec::bp384r1_wnaf::shamir_multiply; +use bouncycastle_ec::nat; + +const LIMBS: usize = 6; + +fn g() -> Bp384r1JacobianPoint { + Bp384r1JacobianPoint::from_affine( + Bp384r1FieldElement::from_limbs(G_X_LIMBS), + Bp384r1FieldElement::from_limbs(G_Y_LIMBS), + ) +} + +fn zero() -> [u64; LIMBS] { + [0; LIMBS] +} + +fn small(v: u64) -> [u64; LIMBS] { + let mut limbs = zero(); + limbs[0] = v; + limbs +} + +fn one() -> [u64; LIMBS] { + small(1) +} + +fn n_minus_1() -> [u64; LIMBS] { + let (limbs, borrow) = nat::sub(&N_LIMBS, &one()); + assert_eq!(borrow, 0); + limbs +} + +/// A pseudorandom value reduced into `[0, n)`, so it is a valid scalar for every multiplier. +fn reduced(limbs: [u64; LIMBS]) -> [u64; LIMBS] { + Bp384r1ScalarField::from_limbs(limbs).to_limbs() +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; LIMBS] { + let mut limbs = zero(); + for limb in limbs.iter_mut() { + *limb = self.next_u64(); + } + limbs + } +} + +#[test] +fn k_times_g_agrees_with_the_comb_multiplier() { + let mut rng = Xorshift64(0x9E3779B97F4A7C15); + let mut cases = vec![one(), small(2), small(3), small(15), small(16), small(17)]; + cases.push(n_minus_1()); + let mut n_minus_2 = n_minus_1(); + n_minus_2[0] -= 1; + cases.push(n_minus_2); + for _ in 0..50 { + cases.push(reduced(rng.next_limbs())); + } + for k in cases { + let via_mul = multiply_point(&Bp384r1Scalar::from_limbs(k), &g()); + let via_comb = comb_multiply_base_point(&Bp384r1Scalar::from_limbs(k)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "k = {k:x?}"); + } +} + +#[test] +fn k_times_q_agrees_with_the_wnaf_multiplier_and_with_the_scalar_field() { + let mut rng = Xorshift64(0xC2B2AE3D27D4EB4F); + for _ in 0..50 { + let a = reduced(rng.next_limbs()); + let k = reduced(rng.next_limbs()); + let q = comb_multiply_base_point(&Bp384r1Scalar::from_limbs(a)); + + let via_mul = multiply_point(&Bp384r1Scalar::from_limbs(k), &q); + + // [0]G + [k]Q by the variable-time multiplier. + let via_wnaf = shamir_multiply( + &Bp384r1PublicScalar::from_limbs(zero()), + &Bp384r1PublicScalar::from_limbs(k), + &q, + ); + assert_eq!(via_mul.to_affine(), via_wnaf.to_affine(), "wnaf: a = {a:x?}, k = {k:x?}"); + + // [a k mod n]G by the fixed-base multiplier. + let ak = + Bp384r1ScalarField::from_limbs(a).mul(&Bp384r1ScalarField::from_limbs(k)).to_limbs(); + let via_comb = comb_multiply_base_point(&Bp384r1Scalar::from_limbs(ak)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "comb: a = {a:x?}, k = {k:x?}"); + } +} + +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let q = g().double(); + assert!(multiply_point(&Bp384r1Scalar::from_limbs(zero()), &q).is_infinity().to_bool()); +} + +#[test] +fn edge_case_k_equals_one_is_q() { + let q = g().double(); + assert_eq!(multiply_point(&Bp384r1Scalar::from_limbs(one()), &q).to_affine(), q.to_affine()); +} + +#[test] +fn edge_case_k_equals_n_minus_1_is_minus_q() { + let q = g().double().add(&g()); + let result = multiply_point(&Bp384r1Scalar::from_limbs(n_minus_1()), &q); + assert_eq!(result.to_affine(), q.negate().to_affine()); +} + +#[test] +fn edge_case_q_is_infinity() { + let k = reduced(Xorshift64(0x2545F4914F6CDD1D).next_limbs()); + assert!( + multiply_point(&Bp384r1Scalar::from_limbs(k), &Bp384r1JacobianPoint::INFINITY) + .is_infinity() + .to_bool() + ); +} + +#[test] +fn negating_q_negates_the_result() { + let k = reduced(Xorshift64(0x1234_5678_9ABC_DEF1).next_limbs()); + let q = g().double(); + let kq = multiply_point(&Bp384r1Scalar::from_limbs(k), &q); + let k_neg_q = multiply_point(&Bp384r1Scalar::from_limbs(k), &q.negate()); + assert_eq!(k_neg_q.to_affine(), kq.negate().to_affine()); +} + +#[test] +fn q_in_a_non_canonical_z_representation_gives_the_same_result() { + // (X, Y, Z) and (λ²X, λ³Y, λZ) are the same affine point; the multiplier must treat them alike. + let k = reduced(Xorshift64(0xDEADBEEFCAFEF00D).next_limbs()); + let q = g().double(); + let lambda = Bp384r1FieldElement::from_limbs(small(7)); + let lambda2 = lambda.mul(&lambda); + let lambda3 = lambda2.mul(&lambda); + let scaled = + Bp384r1JacobianPoint { x: q.x.mul(&lambda2), y: q.y.mul(&lambda3), z: q.z.mul(&lambda) }; + assert_eq!(scaled.to_affine(), q.to_affine(), "the scaled point is the same point"); + assert_eq!( + multiply_point(&Bp384r1Scalar::from_limbs(k), &scaled).to_affine(), + multiply_point(&Bp384r1Scalar::from_limbs(k), &q).to_affine() + ); +} diff --git a/crypto/ec/tests/bp384r1_point_tests.rs b/crypto/ec/tests/bp384r1_point_tests.rs new file mode 100644 index 000000000..08fd1beb4 --- /dev/null +++ b/crypto/ec/tests/bp384r1_point_tests.rs @@ -0,0 +1,115 @@ +//! Known-answer tests for [`Bp384r1JacobianPoint`] arithmetic. +//! +//! `G`, `2G`, `3G` were computed independently in Python via the standard affine group law +//! (general `a`), not from this crate, and cross-checked against `n*G == infinity`: +//! +//! ```text +//! p = 0x8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B412B1DA197FB71123ACD3A729901D1A71874700133107EC53 +//! a = 0x7BC382C63D8C150C3C72080ACE05AFA0C2BEA28E4FB22787139165EFBA91F90F8AA5814A503AD4EB04A8C7DD22CE2826 +//! # affine add/double via the standard general-a formulas +//! ``` + +use bouncycastle_ec::bp384r1::Bp384r1FieldElement; +use bouncycastle_ec::bp384r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp384r1_point::Bp384r1JacobianPoint; + +const TWO_G_X: [u64; 6] = [ + 0xb427b58df9d59fca, 0x7c4ba74a09b553eb, 0xec2f80c4e0f70df8, 0x0ad520b3eb6be4d6, + 0xb95c3495d7b4fd59, 0x2282bc382a2f4dfc, +]; +const TWO_G_Y: [u64; 6] = [ + 0x8f1b29502b6e1d30, 0xb4b4ea69ab64fc28, 0x4d89dd051728fc3e, 0xce9bedbc170921ce, + 0x5768d14a24f37a57, 0x0edda83773ac6873, +]; +const THREE_G_X: [u64; 6] = [ + 0x7d7cc85b3035f11f, 0x3c11ef6596a3b889, 0x3ee1b6fcc7463bbe, 0x3df581348c6949f8, + 0x3b17452b6a27ebf5, 0x7b63205bf00ddae7, +]; +const THREE_G_Y: [u64; 6] = [ + 0x81d7129d48772eb3, 0x73c0edaea3b8f593, 0x7b2bd39462363e03, 0x7b2eb481ead16a5c, + 0x5521a326bc02baaf, 0x761d3a4a5f809377, +]; + +fn fe(limbs: [u64; 6]) -> Bp384r1FieldElement { + Bp384r1FieldElement::from_limbs(limbs) +} + +fn g() -> Bp384r1JacobianPoint { + Bp384r1JacobianPoint::from_affine(fe(G_X_LIMBS), fe(G_Y_LIMBS)) +} + +#[test] +fn known_answer_double() { + let doubled = g().double(); + let (x, y) = doubled.to_affine().unwrap(); + assert_eq!(x, fe(TWO_G_X)); + assert_eq!(y, fe(TWO_G_Y)); +} + +#[test] +fn known_answer_add() { + let sum = g().add(&g().double()); + let (x, y) = sum.to_affine().unwrap(); + assert_eq!(x, fe(THREE_G_X)); + assert_eq!(y, fe(THREE_G_Y)); +} + +#[test] +fn point_plus_negation_is_infinity() { + let g = g(); + assert!(g.add(&g.negate()).is_infinity().to_bool()); +} + +#[test] +fn infinity_identities() { + let g = g(); + let infinity = Bp384r1JacobianPoint::INFINITY; + assert!(infinity.is_infinity().to_bool()); + assert!(infinity.to_affine().is_none()); + + let (x1, y1) = g.add(&infinity).to_affine().unwrap(); + let (x2, y2) = g.to_affine().unwrap(); + assert_eq!((x1, y1), (x2, y2), "P + infinity == P"); + + let (x3, y3) = infinity.add(&g).to_affine().unwrap(); + assert_eq!((x3, y3), (x2, y2), "infinity + P == P"); + + assert!(infinity.add(&infinity).is_infinity().to_bool()); +} + +#[test] +fn scaled_z_representation_still_adds_correctly() { + // (X, Y, Z) and (X*c^2, Y*c^3, Z*c) represent the same affine point for any nonzero c; the + // branch-free add/double formulas must agree regardless of which representative is used. + let c = fe([7, 0, 0, 0, 0, 0]); + let c2 = c.mul(&c); + let c3 = c2.mul(&c); + let g = g(); + let scaled = Bp384r1JacobianPoint { x: g.x.mul(&c2), y: g.y.mul(&c3), z: g.z.mul(&c) }; + + let canonical_double = g.double().to_affine().unwrap(); + let scaled_double = scaled.double().to_affine().unwrap(); + assert_eq!(canonical_double, scaled_double); + + let canonical_sum = g.add(&g.double()).to_affine().unwrap(); + let scaled_sum = scaled.add(&g.double()).to_affine().unwrap(); + assert_eq!(canonical_sum, scaled_sum); +} + +/// `is_infinity` was previously only ever asserted true. The false side is pinned on the base +/// point and on values derived from it, and doubling the identity must stay at the identity +/// (`double` is total, per its docs, and the comb multiplier's accumulator starts there). +#[test] +fn is_infinity_is_false_for_finite_points_and_infinity_doubles_to_itself() { + let g = g(); + assert!(!g.is_infinity().to_bool(), "G is not the identity"); + assert!(!g.double().is_infinity().to_bool(), "2G is not the identity"); + assert!(!g.negate().is_infinity().to_bool(), "-G is not the identity"); + assert!(!g.add(&g.double()).is_infinity().to_bool(), "3G is not the identity"); + + let inf = Bp384r1JacobianPoint::INFINITY; + assert!(inf.double().is_infinity().to_bool(), "2 * infinity == infinity"); + assert!(inf.double().to_affine().is_none()); + assert!(inf.negate().is_infinity().to_bool(), "-infinity == infinity"); + assert!(inf.double().add(&g).to_affine().is_some(), "2 * infinity + G == G, a finite point"); +} diff --git a/crypto/ec/tests/bp384r1_scalar_tests.rs b/crypto/ec/tests/bp384r1_scalar_tests.rs new file mode 100644 index 000000000..cd68a4dbc --- /dev/null +++ b/crypto/ec/tests/bp384r1_scalar_tests.rs @@ -0,0 +1,622 @@ +//! Known-answer tests for [`Bp384r1ScalarField`] arithmetic mod the brainpoolP384r1 curve +//! order `n`. Expected values computed independently in Python: +//! +//! ```text +//! n = 0x8cb91e82a3386d280f5d6f7e50e641df152f7109ed5456b31f166e6cac0425a7cf3ab6af6b7fc3103b883202e9046565 +//! random.seed(830384) +//! vals = [random.randrange(1, n) for _ in range(5)] + [0] +//! # for each pair (i, j) with i < j: (vals[i]+vals[j]) % n, (vals[i]-vals[j]) % n, (vals[i]*vals[j]) % n +//! # for each i: (-vals[i]) % n, pow(vals[i], n-2, n) if vals[i] != 0 else 0 +//! ``` + +use bouncycastle_ec::bp384r1_scalar::{ + Bp384r1PublicScalar, Bp384r1Scalar, Bp384r1ScalarField, N_LIMBS, +}; + +const VALS_0: [u64; 6] = [ + 0x69369a243277814a, 0xede0d2c1b43ac22d, 0x17aaa9a41227ccd6, 0x3203a19dc7d3ce1d, + 0x84019677cea14e16, 0x36c75dc0ce981d6e, +]; +const VALS_1: [u64; 6] = [ + 0x6935e6c751b6ae73, 0x7dc31215cf5f922e, 0xdec4eb2c5423ea30, 0xeb5b4727ca0d7793, + 0xc1f5e7167ab259df, 0x63ec108aea10824b, +]; +const VALS_2: [u64; 6] = [ + 0x36684ffecee01f87, 0xda14ecd2a6850941, 0xb75599c6b4b705f5, 0x94fed47d75f29d1a, + 0x5f1cdd331e5b957e, 0x76cddcd6665f46c3, +]; +const VALS_3: [u64; 6] = [ + 0x797e37ad048c98f2, 0x7d74a6fe383fea19, 0x43c8e04f1f09e5ff, 0xbd38b843a82e8f65, + 0x7600ed2f7ba5343c, 0x08c8e3d7089398b1, +]; +const VALS_4: [u64; 6] = [ + 0x55bdbf0a3b847e80, 0xbed32140bf5ce4d5, 0x6a14533e3da9e911, 0x848a618a268b1b0f, + 0x402db6ac1b0c3e26, 0x31bcb6616eb97973, +]; +const VALS_5: [u64; 6] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, +]; + +const ADD_0_1: [u64; 6] = [ + 0x96e44ee89b29ca58, 0x9c692e28181a914b, 0xd7592663ba47915f, 0x082f77bba48ceefd, + 0x369a0e0ff86d6617, 0x0dfa4fc915703292, +]; +const SUB_0_1: [u64; 6] = [ + 0x3b88e55fc9c5383c, 0x3f58775b505af30f, 0x57fc2ce46a08084e, 0x5bd7cb7feb1aad3c, + 0xd1691edfa4d53615, 0x5f946bb887c0084a, +]; +const MUL_0_1: [u64; 6] = [ + 0xe11d34d8fd807429, 0xbe521073885f1101, 0x3ec19ff2381d7aed, 0x40b18dc0ade49761, + 0x3856a13b9d1b9835, 0x7c473fd290aeba15, +]; +const ADD_0_2: [u64; 6] = [ + 0x6416b82018533b6c, 0xf8bb08e4ef40085e, 0xafe9d4fe1adaad24, 0xb1d3051150721484, + 0xd3c1042c9c16a1b5, 0x20dc1c1491bef709, +]; +const SUB_0_2: [u64; 6] = [ + 0x6e567c284c9bc728, 0xe3069c9e79357bfc, 0x7f6b7e4a0974ec88, 0xb2343e2a3f3587b5, + 0x344228c3012bfa76, 0x4cb29f6d0b7143d3, +]; +const MUL_0_2: [u64; 6] = [ + 0x9df905a6a9a845e0, 0x2d536f240d439a9c, 0x9fee897ffb91f9f8, 0x3dac0f49231c02a3, + 0x943570b62be7fbb5, 0x26aab1f615dcfc71, +]; +const ADD_0_3: [u64; 6] = [ + 0xe2b4d1d137041a3c, 0x6b5579bfec7aac46, 0x5b7389f33131b2d6, 0xef3c59e170025d82, + 0xfa0283a74a468252, 0x3f904197d72bb61f, +]; +const SUB_0_3: [u64; 6] = [ + 0xefb862772deae858, 0x706c2bc37bfad813, 0xd3e1c954f31de6d7, 0x74cae95a1fa53eb7, + 0x0e00a94852fc19d9, 0x2dfe79e9c60484bd, +]; +const MUL_0_3: [u64; 6] = [ + 0x1c4a55020bb0d8fa, 0x77be98f314d4d002, 0xaedc180d60ab43cf, 0xb674a1f0a3603c22, + 0x94414cb9445eee81, 0x5594058d72c9406a, +]; +const ADD_0_4: [u64; 6] = [ + 0xbef4592e6dfbffca, 0xacb3f4027397a702, 0x81befce24fd1b5e8, 0xb68e0327ee5ee92c, + 0xc42f4d23e9ad8c3c, 0x688414223d5196e1, +]; +const SUB_0_4: [u64; 6] = [ + 0x1378db19f6f302ca, 0x2f0db180f4dddd58, 0xad965665d47de3c5, 0xad794013a148b30d, + 0x43d3dfcbb3950fef, 0x050aa75f5fdea3fb, +]; +const MUL_0_4: [u64; 6] = [ + 0xe70d39dc9f46cd70, 0xb3e217c09067341c, 0xd9a8bc2a1147239d, 0x6e007b9384ec7b1c, + 0xfb3d187dfabe316c, 0x4252262c93ad11dd, +]; +const ADD_0_5: [u64; 6] = [ + 0x69369a243277814a, 0xede0d2c1b43ac22d, 0x17aaa9a41227ccd6, 0x3203a19dc7d3ce1d, + 0x84019677cea14e16, 0x36c75dc0ce981d6e, +]; +const SUB_0_5: [u64; 6] = [ + 0x69369a243277814a, 0xede0d2c1b43ac22d, 0x17aaa9a41227ccd6, 0x3203a19dc7d3ce1d, + 0x84019677cea14e16, 0x36c75dc0ce981d6e, +]; +const MUL_0_5: [u64; 6] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, +]; +const ADD_1_2: [u64; 6] = [ + 0x641604c337926895, 0x889d48390a64d85f, 0x770416865cd6ca7e, 0x6b2aaa9b52abbdfb, + 0x11b554cb4827ad7f, 0x4e00cedead375be7, +]; +const SUB_1_2: [u64; 6] = [ + 0x6e55c8cb6bdaf451, 0x72e8dbf2945a4bfd, 0x4685bfd24b7109e2, 0x6b8be3b4416f312c, + 0x72367961ad3d0640, 0x79d7523726e9a8b0, +]; +const MUL_1_2: [u64; 6] = [ + 0x3d78c22f27964af7, 0xe4c5115fbdd2ce40, 0xf64661a7d508f39a, 0x13209addbd589b79, + 0x86e3e27286a9000e, 0x3f322171bc63e9d7, +]; +const ADD_1_3: [u64; 6] = [ + 0xe2b41e7456434765, 0xfb37b914079f7c47, 0x228dcb7b732dd02f, 0xa893ff6b723c06f9, + 0x37f6d445f6578e1c, 0x6cb4f461f2a41afd, +]; +const SUB_1_3: [u64; 6] = [ + 0xefb7af1a4d2a1581, 0x004e6b17971fa814, 0x9afc0add351a0431, 0x2e228ee421dee82e, + 0x4bf4f9e6ff0d25a3, 0x5b232cb3e17ce99a, +]; +const MUL_1_3: [u64; 6] = [ + 0x347b76fb0367c4f2, 0xe0d206cb5fbd5f3e, 0x6400b012b3b24187, 0xe5421fdda01ca282, + 0x0a793ebfe7944964, 0x697eb58f73796c7b, +]; +const ADD_1_4: [u64; 6] = [ + 0x836b73cea436c78e, 0x6d5b7ca7233cb3f3, 0x29c2cffde5c9ad9a, 0x5ab637a803443bf0, + 0xf2c62e4444d85627, 0x08efa869b5918e96, +]; +const SUB_1_4: [u64; 6] = [ + 0x137827bd16322ff3, 0xbeeff0d51002ad59, 0x74b097ee167a011e, 0x66d0e59da3825c84, + 0x81c8306a5fa61bb9, 0x322f5a297b5708d8, +]; +const MUL_1_4: [u64; 6] = [ + 0xd04963394556111c, 0xdc540104a0ae6765, 0x425da4fb66d41e65, 0xff1ac49c5880543e, + 0xac2fc4bb0076dfce, 0x0d763005c3fe70b6, +]; +const ADD_1_5: [u64; 6] = [ + 0x6935e6c751b6ae73, 0x7dc31215cf5f922e, 0xdec4eb2c5423ea30, 0xeb5b4727ca0d7793, + 0xc1f5e7167ab259df, 0x63ec108aea10824b, +]; +const SUB_1_5: [u64; 6] = [ + 0x6935e6c751b6ae73, 0x7dc31215cf5f922e, 0xdec4eb2c5423ea30, 0xeb5b4727ca0d7793, + 0xc1f5e7167ab259df, 0x63ec108aea10824b, +]; +const MUL_1_5: [u64; 6] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, +]; +const ADD_2_3: [u64; 6] = [ + 0xafe687abd36cb879, 0x578993d0dec4f35a, 0xfb1e7a15d3c0ebf5, 0x52378cc11e212c7f, + 0xd51dca629a00c9bb, 0x7f96c0ad6ef2df74, +]; +const SUB_2_3: [u64; 6] = [ + 0xbcea1851ca538695, 0x5ca045d46e451f27, 0x738cb97795ad1ff6, 0xd7c61c39cdc40db5, + 0xe91bf003a2b66141, 0x6e04f8ff5dcbae11, +]; +const MUL_2_3: [u64; 6] = [ + 0x046c3ad519bc030d, 0xd7f6399596b469c6, 0x77a3462d720f2b14, 0xfe2e84ec1666608d, + 0x5d8082a3cef65f56, 0x2184eeccb18a9427, +]; +const ADD_2_4: [u64; 6] = [ + 0x509ddd06216038a2, 0xc9ad5763fa622b06, 0x02537e98465cc95f, 0x0459c4fdaf296177, + 0x8fed2460e88191c6, 0x1bd174b531e0530e, +]; +const SUB_2_4: [u64; 6] = [ + 0xe0aa90f4935ba107, 0x1b41cb91e728246b, 0x4d414688770d1ce4, 0x107472f34f67820b, + 0x1eef2687034f5758, 0x45112674f7a5cd50, +]; +const MUL_2_4: [u64; 6] = [ + 0x0901add57f46737f, 0x6963846e362647ed, 0xa263e90a586b4d67, 0xd136f5b490ba6a6d, + 0x80cf588957238523, 0x386fb27fa2d1b928, +]; +const ADD_2_5: [u64; 6] = [ + 0x36684ffecee01f87, 0xda14ecd2a6850941, 0xb75599c6b4b705f5, 0x94fed47d75f29d1a, + 0x5f1cdd331e5b957e, 0x76cddcd6665f46c3, +]; +const SUB_2_5: [u64; 6] = [ + 0x36684ffecee01f87, 0xda14ecd2a6850941, 0xb75599c6b4b705f5, 0x94fed47d75f29d1a, + 0x5f1cdd331e5b957e, 0x76cddcd6665f46c3, +]; +const MUL_2_5: [u64; 6] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, +]; +const ADD_3_4: [u64; 6] = [ + 0xcf3bf6b740111772, 0x3c47c83ef79cceee, 0xaddd338d5cb3cf11, 0x41c319cdceb9aa74, + 0xb62ea3db96b17263, 0x3a859a38774d1224, +]; +const SUB_3_4: [u64; 6] = [ + 0x5f48aaa5b20c7fd7, 0x8ddc3c6ce462c854, 0xf8cafb7d8d642295, 0x4dddc7c36ef7cb08, + 0x4530a601b17f37f5, 0x63c54bf83d128c66, +]; +const MUL_3_4: [u64; 6] = [ + 0xfd2b75fc2f61086e, 0xadb4e00e5af1807a, 0x74dd012bf60c72e0, 0xe91d5fa5d0778d96, + 0xfff730f0b6c6eb6b, 0x2524434185f031fc, +]; +const ADD_3_5: [u64; 6] = [ + 0x797e37ad048c98f2, 0x7d74a6fe383fea19, 0x43c8e04f1f09e5ff, 0xbd38b843a82e8f65, + 0x7600ed2f7ba5343c, 0x08c8e3d7089398b1, +]; +const SUB_3_5: [u64; 6] = [ + 0x797e37ad048c98f2, 0x7d74a6fe383fea19, 0x43c8e04f1f09e5ff, 0xbd38b843a82e8f65, + 0x7600ed2f7ba5343c, 0x08c8e3d7089398b1, +]; +const MUL_3_5: [u64; 6] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, +]; +const ADD_4_5: [u64; 6] = [ + 0x55bdbf0a3b847e80, 0xbed32140bf5ce4d5, 0x6a14533e3da9e911, 0x848a618a268b1b0f, + 0x402db6ac1b0c3e26, 0x31bcb6616eb97973, +]; +const SUB_4_5: [u64; 6] = [ + 0x55bdbf0a3b847e80, 0xbed32140bf5ce4d5, 0x6a14533e3da9e911, 0x848a618a268b1b0f, + 0x402db6ac1b0c3e26, 0x31bcb6616eb97973, +]; +const MUL_4_5: [u64; 6] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, +]; + +const NEG_0: [u64; 6] = [ + 0xd25197deb68ce41b, 0xe159e3edb74500e2, 0x076bc4c899dc58d0, 0xe32bcf6c25808896, + 0x8b5bd9068244f3c8, 0x55f1c0c1d4a04fb9, +]; +const INV_0: [u64; 6] = [ + 0x15df5ebf73423af2, 0x138f0302e987287d, 0x4b1051978e508fa3, 0x3b727dbac59d9be4, + 0xf07850e679687ce9, 0x69447f8f8d46773a, +]; +const NEG_1: [u64; 6] = [ + 0xd2524b3b974db6f2, 0x5177a4999c2030e1, 0x4051834057e03b77, 0x29d429e22346df1f, + 0x4d678867d633e7ff, 0x28cd0df7b927eadc, +]; +const INV_1: [u64; 6] = [ + 0x20cb4a67f98f7e5b, 0x8324617080bf855c, 0x46b40ba8c1bbe78e, 0x3d2b8ce9d2810ccf, + 0xc56b8a7d57d3a159, 0x5f9a4005d1ceabc2, +]; +const NEG_2: [u64; 6] = [ + 0x051fe2041a2445de, 0xf525c9dcc4fab9cf, 0x67c0d4a5f74d1fb1, 0x80309c8c7761b998, + 0xb040924b328aac60, 0x15eb41ac3cd92664, +]; +const INV_2: [u64; 6] = [ + 0x60f459a1ed5b8d11, 0x17055ca7bc3134b8, 0x1cd5b832e7a489a8, 0x2561aade21101a4a, + 0x1d7c33d4df3a6167, 0x4e20b90d93dc3f4b, +]; +const NEG_3: [u64; 6] = [ + 0xc209fa55e477cc73, 0x51c60fb1333fd8f6, 0xdb4d8e1d8cfa3fa8, 0x57f6b8c64525c74d, + 0x995c824ed5410da2, 0x83f03aab9aa4d476, +]; +const INV_3: [u64; 6] = [ + 0xe89844a1165efa81, 0xec528b21d9ee9849, 0x059fc442e439053e, 0xa97b4d45fde3019c, + 0x34724e6c836c64a8, 0x633c113394fea608, +]; +const NEG_4: [u64; 6] = [ + 0xe5ca72f8ad7fe6e5, 0x1067956eac22de3a, 0xb5021b2e6e5a3c96, 0x90a50f7fc6c93ba3, + 0xcf2fb8d235da03b8, 0x5afc6821347ef3b4, +]; +const INV_4: [u64; 6] = [ + 0xf4d0f4066e1297f1, 0x91b9f7a9bc88454e, 0xf68d9c99331a4c98, 0x93d7a2861b555358, + 0xd8292b0f92b99914, 0x5690c604fdfec994, +]; +const NEG_5: [u64; 6] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, +]; +const INV_5: [u64; 6] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, +]; + +fn fe(limbs: [u64; 6]) -> Bp384r1ScalarField { + Bp384r1ScalarField::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5].map(fe); + let add: Vec> = vec![ + vec![ADD_0_1, ADD_0_2, ADD_0_3, ADD_0_4, ADD_0_5], + vec![ADD_1_2, ADD_1_3, ADD_1_4, ADD_1_5], + vec![ADD_2_3, ADD_2_4, ADD_2_5], + vec![ADD_3_4, ADD_3_5], + vec![ADD_4_5], + ]; + let sub: Vec> = vec![ + vec![SUB_0_1, SUB_0_2, SUB_0_3, SUB_0_4, SUB_0_5], + vec![SUB_1_2, SUB_1_3, SUB_1_4, SUB_1_5], + vec![SUB_2_3, SUB_2_4, SUB_2_5], + vec![SUB_3_4, SUB_3_5], + vec![SUB_4_5], + ]; + let mul: Vec> = vec![ + vec![MUL_0_1, MUL_0_2, MUL_0_3, MUL_0_4, MUL_0_5], + vec![MUL_1_2, MUL_1_3, MUL_1_4, MUL_1_5], + vec![MUL_2_3, MUL_2_4, MUL_2_5], + vec![MUL_3_4, MUL_3_5], + vec![MUL_4_5], + ]; + for i in 0..6 { + for j in (i + 1)..6 { + let expected_add = fe(add[i][j - i - 1]); + let expected_sub = fe(sub[i][j - i - 1]); + let expected_mul = fe(mul[i][j - i - 1]); + assert_eq!(vals[i].add(&vals[j]), expected_add, "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), expected_add, "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), expected_sub, "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), expected_mul, "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), expected_mul, "mul is commutative ({j},{i})"); + } + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5]; + let negs = [NEG_0, NEG_1, NEG_2, NEG_3, NEG_4, NEG_5]; + let invs = [INV_0, INV_1, INV_2, INV_3, INV_4, INV_5]; + for i in 0..6 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5].map(fe); + for &v in &vals { + assert_eq!(v.add(&Bp384r1ScalarField::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&Bp384r1ScalarField::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), Bp384r1ScalarField::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), Bp384r1ScalarField::ZERO, "x + (-x) == 0"); + if v != Bp384r1ScalarField::ZERO { + assert_eq!(v.mul(&v.invert()), Bp384r1ScalarField::ONE, "x * x^-1 == 1"); + } else { + assert_eq!(v.invert(), Bp384r1ScalarField::ZERO, "0^-1 == 0 by convention"); + } + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..6 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + assert_eq!(fe(N_LIMBS), Bp384r1ScalarField::ZERO); + let n_plus_1: [u64; 6] = [ + 0x3b883202e9046566, 0xcf3ab6af6b7fc310, 0x1f166e6cac0425a7, 0x152f7109ed5456b3, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ]; + assert_eq!(fe(n_plus_1), Bp384r1ScalarField::ONE); +} + +#[test] +fn public_scalar_round_trips_and_reduces() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + assert_eq!(Bp384r1PublicScalar::from_limbs(limbs).to_limbs(), limbs); + } + assert_eq!(Bp384r1PublicScalar::from_limbs(N_LIMBS).to_limbs(), [0, 0, 0, 0, 0, 0]); + let n_plus_1: [u64; 6] = [ + 0x3b883202e9046566, 0xcf3ab6af6b7fc310, 0x1f166e6cac0425a7, 0x152f7109ed5456b3, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ]; + assert_eq!(Bp384r1PublicScalar::from_limbs(n_plus_1).to_limbs(), [1, 0, 0, 0, 0, 0]); +} + +#[test] +fn public_scalar_eq_detects_a_difference_in_any_limb() { + let base = Bp384r1PublicScalar::from_limbs(VALS_0); + assert_eq!(base, Bp384r1PublicScalar::from_limbs(VALS_0)); + for limb_idx in 0..6 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!( + base, + Bp384r1PublicScalar::from_limbs(other), + "a difference in limb {limb_idx} must be detected" + ); + } +} + +#[test] +fn secret_scalar_be_bytes_round_trip() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4] { + let secret = Bp384r1Scalar::from_limbs(limbs); + let round_tripped = Bp384r1Scalar::from_be_bytes(&secret.to_be_bytes()); + assert_eq!(round_tripped, secret); + } +} + +#[test] +fn secret_scalar_from_be_bytes_reduces_out_of_range_input() { + let mut n_bytes = [0u8; 48]; + for (i, limb) in N_LIMBS.iter().rev().enumerate() { + n_bytes[i * 8..i * 8 + 8].copy_from_slice(&limb.to_be_bytes()); + } + assert_eq!( + Bp384r1Scalar::from_be_bytes(&n_bytes), + Bp384r1Scalar::from_limbs([0, 0, 0, 0, 0, 0]) + ); +} + +#[test] +fn scalar_field_from_secret_matches_from_limbs() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + let secret = Bp384r1Scalar::from_limbs(limbs); + assert_eq!(Bp384r1ScalarField::from_secret(&secret), fe(limbs)); + } +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property test. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 6] { + [ + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + ] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0xB384256B384256B3); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != Bp384r1ScalarField::ZERO { + assert_eq!(a.mul(&a.invert()), Bp384r1ScalarField::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE000000000E); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 6], + { + let mut l = [0u64; 6]; + l[0] = 1; + l + }, + [u64::MAX; 6], + { + let mut l = [u64::MAX; 6]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `zeroize` is the signing path's way of not leaving `d`, `k` or `k^-1` legible on the stack +/// (see the method's own docs for why this type is not wrapped in `Secret` instead). Whether the +/// volatile write survives optimization cannot be observed from Rust; that the value is actually +/// cleared can be, and is what would break if the method were ever reduced to a no-op. +#[test] +fn zeroize_clears_the_value() { + let mut a = fe([1, 2, 3, 4, 5, 6]); + assert_ne!(a, Bp384r1ScalarField::ZERO, "precondition: the value starts non-zero"); + a.zeroize(); + assert_eq!(a, Bp384r1ScalarField::ZERO, "zeroize must leave the value at zero"); + + // and it is idempotent, so a caller scrubbing twice on overlapping paths is harmless + a.zeroize(); + assert_eq!(a, Bp384r1ScalarField::ZERO); +} + +/// `is_zero` on the scalar field, pinned for both truth values and across every limb position +/// (see the field test of the same name for why); it is the check behind `negate`'s `0 -> 0` +/// special case. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(Bp384r1ScalarField::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::bp384r1_scalar::N_LIMBS).is_zero().to_bool(), "n reduces to 0"); + assert!(!Bp384r1ScalarField::ONE.is_zero().to_bool()); + for limb_idx in 0..6 { + let mut limbs = [0u64; 6]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `n - 1`, the largest canonical scalar, is the worst case for REDC's final conditional +/// subtraction and for `add`'s carry correction, and was absent from every known-answer set +/// above (whose values are pseudorandom, so never near `n`). Expected values follow from +/// `(n-1)^2 == 1` and `(n-1) + (n-1) == n - 2` in the field, computed from `n` in Python. +#[test] +fn known_answer_at_n_minus_1() { + let n_minus_1_limbs: [u64; 6] = [ + 0x3b883202e9046564, 0xcf3ab6af6b7fc310, 0x1f166e6cac0425a7, 0x152f7109ed5456b3, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ]; + let n_minus_1 = fe(n_minus_1_limbs); + let n_minus_2 = fe([ + 0x3b883202e9046563, 0xcf3ab6af6b7fc310, 0x1f166e6cac0425a7, 0x152f7109ed5456b3, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ]); + + assert_eq!(n_minus_1.to_limbs(), n_minus_1_limbs, "Montgomery round trip of n-1"); + assert_eq!(Bp384r1PublicScalar::from_limbs(n_minus_1_limbs).to_limbs(), n_minus_1_limbs); + assert_eq!(n_minus_1.add(&n_minus_1), n_minus_2, "(n-1) + (n-1) == n-2"); + assert_eq!(n_minus_1.add(&Bp384r1ScalarField::ONE), Bp384r1ScalarField::ZERO, "(n-1) + 1 == 0"); + assert_eq!(Bp384r1ScalarField::ZERO.sub(&Bp384r1ScalarField::ONE), n_minus_1, "0 - 1 == n-1"); + assert_eq!(n_minus_1.mul(&n_minus_1), Bp384r1ScalarField::ONE, "(n-1)^2 == 1"); + assert_eq!(n_minus_1.square(), Bp384r1ScalarField::ONE, "(n-1)^2 == 1, via square"); + assert_eq!(n_minus_1.negate(), Bp384r1ScalarField::ONE, "-(n-1) == 1"); + assert_eq!(n_minus_1.invert(), n_minus_1, "(n-1)^-1 == n-1"); + assert_eq!(Bp384r1ScalarField::ONE.invert(), Bp384r1ScalarField::ONE, "1^-1 == 1"); +} + +/// The variable-time inverse on the public scalar type must agree with the constant-time +/// [`Bp384r1ScalarField::invert`] everywhere: on the known-answer values (whose inverses are pinned above +/// from Python), on `0` (both return `0` by convention), `1`, `n - 1`, and on pseudorandom +/// values -- including ones with long runs of zero bits, which are where the binary algorithm +/// takes its longest halving chains. +#[test] +fn public_scalar_invert_vartime_agrees_with_constant_time_invert() { + let n_minus_1 = { + let mut l = bouncycastle_ec::bp384r1_scalar::N_LIMBS; + l[0] -= 1; + l + }; + let mut one = [0u64; 6]; + one[0] = 1; + // Values whose low limb is exactly 1 with higher limbs set: odd, so the binary algorithm must + // not halve them, yet indistinguishable from `1` by their low limb alone. A halving test that + // looked at more than the lowest bit (or at the wrong limb) breaks the `u == x1 * a` invariant + // on exactly these and nowhere the pseudorandom inputs below would reach. + let mut low_limb_one_a = [0u64; 6]; + low_limb_one_a[0] = 1; + low_limb_one_a[1] = 1; + let mut low_limb_one_b = [0u64; 6]; + low_limb_one_b[0] = 1; + low_limb_one_b[5] = 1; + let mut low_limb_one_c = [0u64; 6]; + low_limb_one_c[0] = 1; + low_limb_one_c[1] = u64::MAX; + let mut fixed: Vec<[u64; 6]> = + vec![[0u64; 6], one, n_minus_1, low_limb_one_a, low_limb_one_b, low_limb_one_c]; + fixed.extend([VALS_0, VALS_1, VALS_2]); + for limbs in fixed { + assert_eq!( + bouncycastle_ec::bp384r1_scalar::Bp384r1PublicScalar::from_limbs(limbs) + .invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } + assert_eq!( + bouncycastle_ec::bp384r1_scalar::Bp384r1PublicScalar::from_limbs([0u64; 6]) + .invert_vartime(), + Bp384r1ScalarField::ZERO, + "0^-1 == 0 by convention, matching invert" + ); + + let mut rng = Xorshift64(0x1AC0_B5EC_0000_0006); + for i in 0..500 { + let mut limbs = rng.next_limbs(); + if i % 4 == 0 { + // Sparse values: clear the low half of each limb so `u`/`v` shed many bits per step. + for limb in limbs.iter_mut() { + *limb &= 0xffff_ffff_0000_0000; + } + } + assert_eq!( + bouncycastle_ec::bp384r1_scalar::Bp384r1PublicScalar::from_limbs(limbs) + .invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } +} diff --git a/crypto/ec/tests/bp384r1_sec1_tests.rs b/crypto/ec/tests/bp384r1_sec1_tests.rs new file mode 100644 index 000000000..848acd1ac --- /dev/null +++ b/crypto/ec/tests/bp384r1_sec1_tests.rs @@ -0,0 +1,145 @@ +//! Known-answer tests for SEC 1 §2.3.3/§2.3.4 point encoding and decoding, brainpoolP384r1. +//! +//! `G`'s coordinates are RFC 5639 §3.4's (already verified independently in +//! `bp384r1_point_tests.rs`); `4G`'s were computed independently in Python via the standard +//! general-`a` affine group law, specifically because it has even `y` where `G`'s (and `2G`'s, +//! `3G`'s) is odd, so between them both compressed tags (`0x02`/`0x03`) are exercised. The +//! encodings below were derived from those coordinates independently in Python via plain +//! big-endian byte packing. + +use bouncycastle_ec::bp384r1::Bp384r1FieldElement; +use bouncycastle_ec::bp384r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp384r1_sec1::{decode, decode_point, encode_compressed, encode_uncompressed}; + +const FOUR_G_X: [u64; 6] = [ + 0xe48921ec979f4a32, 0x1a345294d5446828, 0x8d4462d702c844e1, 0xe45a6dca90945dba, + 0x0675d5abc72ebc2a, 0x0dd5393f5c885956, +]; +const FOUR_G_Y: [u64; 6] = [ + 0x103b90824e878ba2, 0x6fce80c25f70c7af, 0x071ec79036c5f467, 0x832cf70a0254f70f, + 0xad184f44d3ab5005, 0x80ffdfa1ea4fdf56, +]; + +const UNCOMPRESSED_G: [u8; 97] = [ + 0x04, 0x1d, 0x1c, 0x64, 0xf0, 0x68, 0xcf, 0x45, 0xff, 0xa2, 0xa6, 0x3a, 0x81, 0xb7, 0xc1, 0x3f, + 0x6b, 0x88, 0x47, 0xa3, 0xe7, 0x7e, 0xf1, 0x4f, 0xe3, 0xdb, 0x7f, 0xca, 0xfe, 0x0c, 0xbd, 0x10, + 0xe8, 0xe8, 0x26, 0xe0, 0x34, 0x36, 0xd6, 0x46, 0xaa, 0xef, 0x87, 0xb2, 0xe2, 0x47, 0xd4, 0xaf, + 0x1e, 0x8a, 0xbe, 0x1d, 0x75, 0x20, 0xf9, 0xc2, 0xa4, 0x5c, 0xb1, 0xeb, 0x8e, 0x95, 0xcf, 0xd5, + 0x52, 0x62, 0xb7, 0x0b, 0x29, 0xfe, 0xec, 0x58, 0x64, 0xe1, 0x9c, 0x05, 0x4f, 0xf9, 0x91, 0x29, + 0x28, 0x0e, 0x46, 0x46, 0x21, 0x77, 0x91, 0x81, 0x11, 0x42, 0x82, 0x03, 0x41, 0x26, 0x3c, 0x53, + 0x15, +]; + +const COMPRESSED_G: [u8; 49] = [ + 0x03, 0x1d, 0x1c, 0x64, 0xf0, 0x68, 0xcf, 0x45, 0xff, 0xa2, 0xa6, 0x3a, 0x81, 0xb7, 0xc1, 0x3f, + 0x6b, 0x88, 0x47, 0xa3, 0xe7, 0x7e, 0xf1, 0x4f, 0xe3, 0xdb, 0x7f, 0xca, 0xfe, 0x0c, 0xbd, 0x10, + 0xe8, 0xe8, 0x26, 0xe0, 0x34, 0x36, 0xd6, 0x46, 0xaa, 0xef, 0x87, 0xb2, 0xe2, 0x47, 0xd4, 0xaf, + 0x1e, +]; + +const UNCOMPRESSED_FOUR_G: [u8; 97] = [ + 0x04, 0x0d, 0xd5, 0x39, 0x3f, 0x5c, 0x88, 0x59, 0x56, 0x06, 0x75, 0xd5, 0xab, 0xc7, 0x2e, 0xbc, + 0x2a, 0xe4, 0x5a, 0x6d, 0xca, 0x90, 0x94, 0x5d, 0xba, 0x8d, 0x44, 0x62, 0xd7, 0x02, 0xc8, 0x44, + 0xe1, 0x1a, 0x34, 0x52, 0x94, 0xd5, 0x44, 0x68, 0x28, 0xe4, 0x89, 0x21, 0xec, 0x97, 0x9f, 0x4a, + 0x32, 0x80, 0xff, 0xdf, 0xa1, 0xea, 0x4f, 0xdf, 0x56, 0xad, 0x18, 0x4f, 0x44, 0xd3, 0xab, 0x50, + 0x05, 0x83, 0x2c, 0xf7, 0x0a, 0x02, 0x54, 0xf7, 0x0f, 0x07, 0x1e, 0xc7, 0x90, 0x36, 0xc5, 0xf4, + 0x67, 0x6f, 0xce, 0x80, 0xc2, 0x5f, 0x70, 0xc7, 0xaf, 0x10, 0x3b, 0x90, 0x82, 0x4e, 0x87, 0x8b, + 0xa2, +]; + +const COMPRESSED_FOUR_G: [u8; 49] = [ + 0x02, 0x0d, 0xd5, 0x39, 0x3f, 0x5c, 0x88, 0x59, 0x56, 0x06, 0x75, 0xd5, 0xab, 0xc7, 0x2e, 0xbc, + 0x2a, 0xe4, 0x5a, 0x6d, 0xca, 0x90, 0x94, 0x5d, 0xba, 0x8d, 0x44, 0x62, 0xd7, 0x02, 0xc8, 0x44, + 0xe1, 0x1a, 0x34, 0x52, 0x94, 0xd5, 0x44, 0x68, 0x28, 0xe4, 0x89, 0x21, 0xec, 0x97, 0x9f, 0x4a, + 0x32, +]; + +fn fe(limbs: [u64; 6]) -> Bp384r1FieldElement { + Bp384r1FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_encode_uncompressed() { + assert_eq!(encode_uncompressed(&fe(G_X_LIMBS), &fe(G_Y_LIMBS)), UNCOMPRESSED_G); + assert_eq!(encode_uncompressed(&fe(FOUR_G_X), &fe(FOUR_G_Y)), UNCOMPRESSED_FOUR_G); +} + +#[test] +fn known_answer_encode_compressed() { + assert_eq!( + encode_compressed(&fe(G_X_LIMBS), &fe(G_Y_LIMBS)), + COMPRESSED_G, + "G has odd y -> tag 0x03" + ); + assert_eq!( + encode_compressed(&fe(FOUR_G_X), &fe(FOUR_G_Y)), + COMPRESSED_FOUR_G, + "4G has even y -> tag 0x02" + ); +} + +#[test] +fn known_answer_decode_uncompressed() { + let (x, y) = decode(&UNCOMPRESSED_G).expect("valid uncompressed point"); + assert_eq!(x, fe(G_X_LIMBS)); + assert_eq!(y, fe(G_Y_LIMBS)); +} + +#[test] +fn known_answer_decode_compressed() { + let (x, y) = decode(&COMPRESSED_G).expect("valid compressed point, tag 0x03"); + assert_eq!(x, fe(G_X_LIMBS)); + assert_eq!(y, fe(G_Y_LIMBS)); + + let (x4, y4) = decode(&COMPRESSED_FOUR_G).expect("valid compressed point, tag 0x02"); + assert_eq!(x4, fe(FOUR_G_X)); + assert_eq!(y4, fe(FOUR_G_Y)); +} + +#[test] +fn round_trip_uncompressed_and_compressed() { + for (x, y) in [(G_X_LIMBS, G_Y_LIMBS), (FOUR_G_X, FOUR_G_Y)] { + let uncompressed = encode_uncompressed(&fe(x), &fe(y)); + assert_eq!(decode(&uncompressed), Some((fe(x), fe(y)))); + + let compressed = encode_compressed(&fe(x), &fe(y)); + assert_eq!(decode(&compressed), Some((fe(x), fe(y)))); + + assert!(decode_point(&uncompressed).unwrap().to_affine().is_some()); + } +} + +#[test] +fn decode_rejects_malformed_input() { + assert_eq!(decode(&[]), None); + assert_eq!(decode(&[0x04; 96]), None); + assert_eq!(decode(&[0x04; 98]), None); + assert_eq!(decode(&[0x02; 16]), None); + + let mut bad_tag_97 = UNCOMPRESSED_G; + bad_tag_97[0] = 0x02; + assert_eq!(decode(&bad_tag_97), None); + + let mut bad_tag_49 = COMPRESSED_G; + bad_tag_49[0] = 0x04; + assert_eq!(decode(&bad_tag_49), None); + + assert_eq!(decode(&[0x00]), None); + + let mut x_too_big = COMPRESSED_G; + x_too_big[1..49].fill(0xff); + assert_eq!(decode(&x_too_big), None); + + let mut no_root = COMPRESSED_G; + no_root[1..49].fill(0); + no_root[48] = 2; // x = 2, confirmed (in the Python derivation) to have no square root + assert_eq!(decode(&no_root), None); + + let mut not_on_curve = UNCOMPRESSED_G; + not_on_curve[96] ^= 0x01; + assert_eq!(decode(¬_on_curve), None); + + let mut y_too_big = UNCOMPRESSED_G; + y_too_big[49..97].fill(0xff); + assert_eq!(decode(&y_too_big), None); +} diff --git a/crypto/ec/tests/bp384r1_wnaf_tests.rs b/crypto/ec/tests/bp384r1_wnaf_tests.rs new file mode 100644 index 000000000..b039026c9 --- /dev/null +++ b/crypto/ec/tests/bp384r1_wnaf_tests.rs @@ -0,0 +1,181 @@ +//! Known-answer tests for [`shamir_multiply`] (`[u]G + [v]Q`), brainpoolP384r1. +//! +//! `Q` below is `d*G` for a fixed test-only `d` (not a real key). Expected results were computed +//! independently in Python from the affine group law (general `a`), the same way as +//! `bp384r1_comb_tests.rs`, with `random.seed(808384)`. + +use bouncycastle_ec::bp384r1::Bp384r1FieldElement; +use bouncycastle_ec::bp384r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp384r1_point::Bp384r1JacobianPoint; +use bouncycastle_ec::bp384r1_scalar::{Bp384r1PublicScalar, Bp384r1Scalar}; +use bouncycastle_ec::bp384r1_wnaf::shamir_multiply; + +const Q_X: [u64; 6] = [ + 0x5b44524010bc5404, 0x4357db763fd6cab8, 0x1db62b723f57e3ae, 0x848f29c8305dfe90, + 0xa25d398c748f5185, 0x34664177dbe1b9a0, +]; +const Q_Y: [u64; 6] = [ + 0xb65bd155583f26ac, 0xeecdd7a2b2310c1d, 0x7719da95a7acae74, 0xa30d83a1c4c00d57, + 0x97617636091ba79a, 0x600741dd2ab16b2c, +]; + +fn q() -> Bp384r1JacobianPoint { + Bp384r1JacobianPoint::from_affine( + Bp384r1FieldElement::from_limbs(Q_X), + Bp384r1FieldElement::from_limbs(Q_Y), + ) +} + +fn scalar(limbs: [u64; 6]) -> Bp384r1PublicScalar { + Bp384r1PublicScalar::from_limbs(limbs) +} + +fn assert_result_is(u: [u64; 6], v: [u64; 6], expected_x: [u64; 6], expected_y: [u64; 6]) { + let result = shamir_multiply(&scalar(u), &scalar(v), &q()); + let (rx, ry) = result.to_affine().expect("expected a non-infinity result"); + assert_eq!(rx, Bp384r1FieldElement::from_limbs(expected_x), "x mismatch for u={u:x?} v={v:x?}"); + assert_eq!(ry, Bp384r1FieldElement::from_limbs(expected_y), "y mismatch for u={u:x?} v={v:x?}"); +} + +#[test] +fn known_answer_large_u_v() { + let u: [u64; 6] = [ + 0xab460788b359c7e8, 0xc53f364b2831dc1d, 0x88aebe98b4707af4, 0x0cb0e573234caea1, + 0xba948bc69341a076, 0x53880bbb2c34e9eb, + ]; + let v: [u64; 6] = [ + 0xd01cc5f6019abebb, 0x1e33bc48f173dd98, 0x1778851a97225d6e, 0x1f8af7a981f2e7c1, + 0xfb3507316731ab74, 0x186bcc03ec975e9a, + ]; + assert_result_is( + u, + v, + [ + 0x532067c358722c9f, 0xa9afb5f0af4af40a, 0x65ae351aeab9991e, 0xb364aad77b1c0f7e, + 0x8dceb0ad73f0e2e3, 0x1a8ca748362e8d33, + ], + [ + 0x57eea0309aa209b7, 0xc971d41f677ad5f8, 0x237b73a259c13d6e, 0xb0c52f7bef139b37, + 0xe848cdee1beff0f9, 0x45e79622a771e8a6, + ], + ); +} + +#[test] +fn edge_case_v_is_zero_reduces_to_u_times_g() { + assert_result_is( + [5, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [ + 0x2cebab9fd2412dfb, 0xfbc1247651c2770d, 0x0ac96ddf237f84f4, 0x465848a4b4fbb608, + 0x5100dabea7b5f59f, 0x0d3ec4dfce264772, + ], + [ + 0x3f9b162e47634690, 0xe059c219dfbbc32b, 0x97495c4579bb950c, 0x39f00d1d90ed0c6d, + 0xebaa167fa90635f9, 0x20168ac65e9bb101, + ], + ); +} + +#[test] +fn edge_case_u_is_zero_reduces_to_v_times_q() { + assert_result_is( + [0, 0, 0, 0, 0, 0], + [5, 0, 0, 0, 0, 0], + [ + 0x3f7fb20be3020eaf, 0x888e9d034b87f935, 0x3ebc83d2f22eca11, 0x666e10424c2870bf, + 0xaf17628bc72875ee, 0x892c40998e25e398, + ], + [ + 0x67395cb642b58fda, 0x24b2a960b1355af4, 0x377593ca4162ca87, 0x9a874c7a3f4b27eb, + 0x3c16b0813a41f150, 0x671afc917054aac4, + ], + ); +} + +#[test] +fn edge_case_both_zero_is_infinity() { + let result = shamir_multiply(&scalar([0; 6]), &scalar([0; 6]), &q()); + assert!(result.to_affine().is_none()); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 6] { + [ + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + ] + } +} + +#[test] +fn cross_checked_against_the_comb_multiplier_over_many_pseudorandom_scalars() { + // shamir_multiply(k, 0, anything) must equal comb_multiply_base_point(k): two independently + // implemented multipliers (fixed-base comb vs. interleaved wNAF) computing the same [k]G. + let mut rng = Xorshift64(0xE7C558A6A6E7F1DE); + for _ in 0..500 { + let k_limbs = rng.next_limbs(); + let via_comb = comb_multiply_base_point(&Bp384r1Scalar::from_limbs(k_limbs)); + let via_wnaf = + shamir_multiply(&Bp384r1PublicScalar::from_limbs(k_limbs), &scalar([0; 6]), &q()); + assert_eq!(via_comb.to_affine(), via_wnaf.to_affine(), "k = {k_limbs:x?}"); + } +} + +/// Every other test in this file uses one fixed `Q = dG`. The three `Q` values here each take a +/// path that one cannot: `Q = infinity` makes every `[v]Q` digit an addition of the identity, +/// `Q = G` makes the two precomputed odd-multiple tables identical (so the interleaved +/// additions hit the same-point case of `add_vartime`), and `Q = -G` makes them negatives of +/// each other (the opposite-point case, and with `u == v` a final result of infinity). Expected +/// values come from the fixed-base comb multiplier on the combined scalar, which is pinned by +/// its own known-answer tests. +#[test] +fn edge_case_q_is_infinity_g_or_minus_g() { + let g = Bp384r1JacobianPoint::from_affine( + Bp384r1FieldElement::from_limbs(bouncycastle_ec::bp384r1_domain::G_X_LIMBS), + Bp384r1FieldElement::from_limbs(bouncycastle_ec::bp384r1_domain::G_Y_LIMBS), + ); + let mut u = [0u64; 6]; + u[0] = 12345; + let mut v = [0u64; 6]; + v[0] = 6789; + let mut u_plus_v = [0u64; 6]; + u_plus_v[0] = 19134; + let mut u_minus_v = [0u64; 6]; + u_minus_v[0] = 5556; + let via_comb = + |k: [u64; 6]| comb_multiply_base_point(&Bp384r1Scalar::from_limbs(k)).to_affine(); + + // [u]G + [v]infinity == [u]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &Bp384r1JacobianPoint::INFINITY).to_affine(), + via_comb(u) + ); + // [u]G + [v]G == [u+v]G + assert_eq!(shamir_multiply(&scalar(u), &scalar(v), &g).to_affine(), via_comb(u_plus_v)); + // [u]G + [v](-G) == [u-v]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &g.negate()).to_affine(), + via_comb(u_minus_v) + ); + // [u]G + [u](-G) == infinity + assert!(shamir_multiply(&scalar(u), &scalar(u), &g.negate()).is_infinity().to_bool()); + // [0]G + [v]G == [v]G: the G table is never consulted, only Q's + assert_eq!(shamir_multiply(&scalar([0u64; 6]), &scalar(v), &g).to_affine(), via_comb(v)); +} diff --git a/crypto/ec/tests/bp512r1_comb_tests.rs b/crypto/ec/tests/bp512r1_comb_tests.rs new file mode 100644 index 000000000..820f67680 --- /dev/null +++ b/crypto/ec/tests/bp512r1_comb_tests.rs @@ -0,0 +1,95 @@ +//! Known-answer tests for [`comb_multiply_base_point`] (`[k]G`), brainpoolP512r1. +//! +//! `k = 2, 3` reuse the values verified independently in `bp512r1_point_tests.rs`; `k` for +//! `known_answer_large_k` and `n-1` (where `[k]G = -G`) were computed the same way (Python affine +//! group law, general `a`), `random.seed(628512)` for the former. + +use bouncycastle_ec::bp512r1::Bp512r1FieldElement; +use bouncycastle_ec::bp512r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp512r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp512r1_scalar::Bp512r1Scalar; + +fn scalar(limbs: [u64; 8]) -> Bp512r1Scalar { + Bp512r1Scalar::from_limbs(limbs) +} + +fn assert_result_is(k_limbs: [u64; 8], x: [u64; 8], y: [u64; 8]) { + let result = comb_multiply_base_point(&scalar(k_limbs)); + let (rx, ry) = result.to_affine().expect("[k]G must not be infinity for k in (0, n)"); + assert_eq!(rx, Bp512r1FieldElement::from_limbs(x), "x mismatch for k = {k_limbs:x?}"); + assert_eq!(ry, Bp512r1FieldElement::from_limbs(y), "y mismatch for k = {k_limbs:x?}"); +} + +#[test] +fn known_answer_small_k() { + assert_result_is([1, 0, 0, 0, 0, 0, 0, 0], G_X_LIMBS, G_Y_LIMBS); + assert_result_is( + [2, 0, 0, 0, 0, 0, 0, 0], + [ + 0x080447def02f4850, 0xf698f404089a4cc5, 0x97116e702915a4f4, 0x957877f3a8f0f725, + 0xa30d3035f4cb6581, 0xd18d8141b8a18064, 0x0a63285758f399b3, 0x9f4945f680edf980, + ], + [ + 0xe269ad21be592e71, 0x63a42e4181fd929c, 0xa5c3b602b0960cbf, 0xfdf99bb1709c700f, + 0x5a2c366b03e5d1b2, 0x149ce1238d3f1e0f, 0x49826b716292f29d, 0x6d6b4b188b699c56, + ], + ); + assert_result_is( + [3, 0, 0, 0, 0, 0, 0, 0], + [ + 0x5d4abf882ccb8d94, 0xf56e34abfa9ac720, 0x4780ae53e1853d62, 0xc1f6fb975ceecade, + 0x09c09cefd830151b, 0x907c80ef3bc24593, 0x36cdd42543f20afe, 0x08dd87e12b0a4cc4, + ], + [ + 0x5cdeafba05b02c37, 0x8cce5bc75d8de649, 0xfebfcb69c0f37c5f, 0xacdab8eb772327b3, + 0xba0b382e1716d843, 0x43d903b4a6334c4b, 0x756ff0067376fa75, 0x026ef5c6e1dab71d, + ], + ); +} + +#[test] +fn known_answer_large_k() { + let k: [u64; 8] = [ + 0x87853ca374a7f4e4, 0xae79c9c15dca288b, 0xe330aea7be2add07, 0x0a579fad505bf7b1, + 0x51f409ed641a5248, 0x392469d4a9acbb2d, 0x53c9bde28cb547c0, 0x7de160dffa4cc3d1, + ]; + assert_result_is( + k, + [ + 0x534af60225340e7d, 0xe6db088083d684b3, 0xde9649b05f5e52d7, 0x429fbc57716a8a9f, + 0xa1cb1bd40c08f996, 0xab75449a47e5e522, 0xd4211cccea3aff37, 0x0ed19b96369de5cc, + ], + [ + 0x13405e9186c46db0, 0xe1e511ffb03ea95c, 0xb539aa547ccec7c9, 0x0c25f04aef0c65e4, + 0xd655f73c2cbd06da, 0x4b7a7fe305671709, 0x50469ea0652c8ee7, 0x9cf1116ec20e9e70, + ], + ); +} + +#[test] +fn edge_case_k_equals_n_minus_1() { + // (n-1)*G == -G: same x as G, negated y. + let n_minus_1: [u64; 8] = [ + 0xb58796829ca90068, 0x1db1d381085ddadd, 0x418661197fac1047, 0x553e5c414ca92619, + 0xd6639cca70330870, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ]; + assert_result_is( + n_minus_1, + G_X_LIMBS, + [ + 0xafdd42471d624061, 0x56b7d3ff8492727e, 0x530355525c7c1d37, 0xca70bcb751671fe4, + 0x3662d76ee813875f, 0xd92696b38f2456f4, 0x7eea27046451d909, 0x2cff655b8586919e, + ], + ); +} + +/// `k = 0` is the one scalar for which `[k]G` is the identity: every comb digit is `0`, so every +/// round selects table entry `0` (the infinity sentinel) and the accumulator never leaves +/// `INFINITY`. Nothing in the signing path can produce it (`k` is in `[1, n-1]` by construction), +/// which is exactly why it needs pinning here rather than being left to chance. +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let result = comb_multiply_base_point(&scalar([0u64; 8])); + assert!(result.is_infinity().to_bool()); + assert!(result.to_affine().is_none()); +} diff --git a/crypto/ec/tests/bp512r1_extra_bits_tests.rs b/crypto/ec/tests/bp512r1_extra_bits_tests.rs new file mode 100644 index 000000000..efa5b22fb --- /dev/null +++ b/crypto/ec/tests/bp512r1_extra_bits_tests.rs @@ -0,0 +1,143 @@ +//! Known-answer tests for [`reduce_wide_bits_mod_n_minus_1`] (FIPS 186-5 Appendix A.4.1 steps +//! 3-5): `x = bits2int(X)`, `x = x mod (n-1)`, `x = x + 1`. Expected values computed independently +//! in Python via `(int.from_bytes(bytes, 'big') % (n - 1)) + 1` with `n` from RFC 5639 §3.4, not +//! derived from this crate's own reduction code -- see `extra_bits_bp256r1_tests.rs`'s docs for why +//! this independent-reference pinning matters (a real test gap `cargo mutants` found on that +//! curve's analogous constant and reduction). + +use bouncycastle_ec::bp512r1_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_hex::decode as hex_decode; + +fn bytes64(hex: &str) -> [u8; 64] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +#[test] +fn known_answer_repeating_pattern() { + let input: Vec = [0x01u8, 0x02, 0x03, 0x04].iter().cloned().cycle().take(72).collect(); + let expected = bytes64( + "2cba3745dd71ed5c4cc168f9f67d3c7e18f888d946d40f4564a04fd7bfc177c94c477653df36f73070d93ffa1bab888928286601549d218f6b6de368e4089705", + ); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_ff() { + let input = [0xffu8; 72]; + let expected = bytes64( + "0e92f4a257e37a20a2fb93f3a339141b36cf5e509308f63b090de57b2a66d2e19da0600d96652b67b2e40e271d3a86916d3d726abda6c95cef9691d720f64ff0", + ); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_zero_yields_one() { + // bits2int(0) = 0; 0 mod (n-1) = 0; +1 = 1 -- the smallest value Appendix A.4.1 can produce. + let input = [0u8; 72]; + let mut expected = [0u8; 64]; + expected[63] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +/// The two boundaries of Appendix A.4.1's own arithmetic: `n - 1` is the modulus of step 4, so it +/// reduces to `0` and step 5 makes it `1`; `n - 2` is the largest residue, so it comes out as +/// `n - 1`, the top of the `[1, n-1]` output interval. Both inputs are left-padded to the DRBG +/// output width this module is fed with. +#[test] +fn known_answer_at_the_n_minus_1_boundary() { + let n = bouncycastle_ec::bp512r1_sec1::be_bytes_from_limbs( + &bouncycastle_ec::bp512r1_scalar::N_LIMBS, + ); + let mut n_minus_1 = n; + n_minus_1[63] -= 1; // n is odd, so neither of these subtractions borrows + let mut n_minus_2 = n; + n_minus_2[63] -= 2; + + let mut input = [0u8; 72]; + input[8..].copy_from_slice(&n_minus_1); + let mut one = [0u8; 64]; + one[63] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), one, "n - 1 -> 1"); + + input[8..].copy_from_slice(&n_minus_2); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), n_minus_1, "n - 2 -> n - 1"); +} + +/// The bit-serial algorithm this module implemented before switching to Barrett reduction, kept +/// here as an independent reference: Horner's rule over the input bits, doubling the running +/// remainder and subtracting `n - 1` when it is exceeded, with the carry out of the top limb folded +/// back in as `2^512 mod (n-1) = 2^512 - (n-1)`. Slow, obviously correct, and sharing no +/// code with the Barrett path beyond `nat`'s add and sub. +fn reference_bit_serial(bytes: &[u8]) -> [u8; 64] { + use bouncycastle_ec::nat; + let n = bouncycastle_ec::bp512r1_scalar::N_LIMBS; + let mut m = n; + m[0] -= 1; // n is odd + let two_pow_mod_m = nat::sub(&[0u64; 8], &m).0; // 2^512 - m, wrapping + let mut acc = [0u64; 8]; + for &byte in bytes { + for bit_idx in (0..8).rev() { + let (mut doubled, carry) = nat::add(&acc, &acc); + doubled[0] |= ((byte >> bit_idx) & 1) as u64; + if carry == 1 { + doubled = nat::add(&doubled, &two_pow_mod_m).0; + } + let (reduced, borrow) = nat::sub(&doubled, &m); + acc = if borrow == 1 { doubled } else { reduced }; + } + } + let plus_one = nat::add(&acc, &[1u64, 0, 0, 0, 0, 0, 0, 0]).0; + bouncycastle_ec::bp512r1_sec1::be_bytes_from_limbs(&plus_one) +} + +/// xorshift64*, fixed seed, as in the `ec` crate's property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } +} + +/// Barrett against the bit-serial reference over pseudorandom inputs at every width from one byte +/// up to the 127-byte maximum the function accepts, biased towards the widths actually drawn +/// (72 bytes) and towards values with long runs of set bits, where Barrett's quotient estimate is +/// furthest off and the most conditional subtractions are needed. +#[test] +fn agrees_with_the_bit_serial_reference_over_many_pseudorandom_inputs() { + let mut rng = Xorshift64(0xBA77E770_0000_0008); + for i in 0..3000 { + let len = match i % 4 { + 0 => 72, + 1 => 127, + _ => 1 + (rng.next_u64() as usize % 127), + }; + let mut bytes = vec![0u8; len]; + for chunk in bytes.chunks_mut(8) { + let word = rng.next_u64().to_be_bytes(); + chunk.copy_from_slice(&word[..chunk.len()]); + } + if i % 3 == 0 { + for b in bytes.iter_mut().take(len / 2) { + *b = 0xff; + } + } + assert_eq!( + reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), + reference_bit_serial(&bytes), + "len = {len}, bytes = {bytes:02x?}" + ); + } +} + +#[test] +fn maximum_width_all_ones_input() { + // 2^1016 - 1, the largest input accepted: Barrett's precondition holds with room to spare. + let bytes = [0xffu8; 127]; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), reference_bit_serial(&bytes)); +} diff --git a/crypto/ec/tests/bp512r1_field_tests.rs b/crypto/ec/tests/bp512r1_field_tests.rs new file mode 100644 index 000000000..6aae6fddd --- /dev/null +++ b/crypto/ec/tests/bp512r1_field_tests.rs @@ -0,0 +1,427 @@ +//! Known-answer tests for [`Bp512r1FieldElement`] arithmetic. Expected values computed +//! independently in Python: +//! +//! ```text +//! p = 0xaadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca703308717d4d9b009bc66842aecda12ae6a380e62881ff2f2d82c68528aa6056583a48f3 +//! random.seed(512256) +//! vals = [random.randrange(1, p) for _ in range(3)] +//! ``` + +use bouncycastle_ec::bp512r1::Bp512r1FieldElement; + +const VALS_0: [u64; 8] = [ + 0x337b305d3dae11f7, 0x9a0e685d00e50ffb, 0x20de024e6e702163, 0xc6cbe74f04fffde6, + 0x1aa4a7e8de900164, 0x7020023b3948ad73, 0xe38738b2110eeb8a, 0x9ac32dabfdf6cc6a, +]; +const VALS_1: [u64; 8] = [ + 0xb5a040b9e9952d99, 0x8d4827f2d807fb55, 0xf6a40ce0710df9e9, 0xcc17138a5e9399a6, + 0xe8bcdbef0eaea63e, 0x2ccc1c0bbfca1304, 0x9c49d912867eb4fd, 0x5ca72cdde0671ebd, +]; +const VALS_2: [u64; 8] = [ + 0xcef9df78cab56bed, 0x7e73181220467918, 0x11e27aad93bf8bf2, 0xa7cb1c3a8e7a0340, + 0xdf0f27f3dec18923, 0xd8d645667d6eb048, 0xe8ff417831c028e4, 0x7a65f1457edd80cd, +]; + +const ADD_0_1: [u64; 8] = [ + 0xc07110c0cf08f69d, 0xfed49120ab6a44cb, 0x68b46e03f8da9a66, 0x15955fd8c7cd2f4a, + 0x2cfde70d7d0b9f32, 0xd1bb90934548ee69, 0x3ffc2b1663c3a47f, 0x4c8cbcd10274269d, +]; +const SUB_0_1: [u64; 8] = [ + 0x7ddaefa35418e45e, 0x0cc6406a28dd14a5, 0x2a39f56dfd62277a, 0xfab4d3c4a66c643f, + 0x31e7cbf9cfe15b25, 0x4353e62f797e9a6e, 0x473d5f9f8a90368d, 0x3e1c00ce1d8fadad, +]; +const MUL_0_1: [u64; 8] = [ + 0x7fd7e4e5ba099749, 0xc172dfaab2d795bc, 0x52316d19b6ea8913, 0x3ba73c885e93ca3d, + 0x5a58f3dbbc4862ae, 0x030cfe3ae6b57e73, 0xec26fbc5794dbabd, 0x4e49089233647524, +]; +const ADD_0_2: [u64; 8] = [ + 0xd9caaf7fb02934f1, 0xefff813ff3a8c28e, 0x83f2dbd11b8c2c6f, 0xf1496888f7b398e3, + 0x235033124d1e8216, 0x7dc5b9ee02ed8bad, 0x8cb1937c0f051867, 0x6a4b8138a0ea88ad, +]; +const SUB_0_2: [u64; 8] = [ + 0x648150e472f8a60a, 0x1b9b504ae09e96e2, 0x0efb87a0dab09571, 0x1f00cb147685faa6, + 0x3b957ff4ffce7841, 0x9749bcd4bbd9fd2a, 0xfa87f739df4ec2a5, 0x205d3c667f194b9c, +]; +const MUL_0_2: [u64; 8] = [ + 0x8e044ad76f99bdd8, 0x42e41dbf91bec416, 0xe7de54a2bb0da823, 0xea4d40c0a249aa3e, + 0xefbb09a11cf5b168, 0xc9d10888667929b4, 0x656b4aed0849f1bd, 0x0f720ad097add84a, +]; +const ADD_1_2: [u64; 8] = [ + 0x5befbfdc5c105093, 0xe33940d5cacbade9, 0x59b8e6631e2a04f5, 0xf69494c4514734a4, + 0xf16867187d3d26f0, 0x3a71d3be896ef13e, 0x457433dc8474e1da, 0x2c2f806a835adb00, +]; +const SUB_1_2: [u64; 8] = [ + 0x0f50c197771a0a9f, 0x37570f0fe54448c2, 0x938f335dc3f1eedd, 0xa19992506bdffea9, + 0xe01150c5a020258c, 0x1f266458f62534ca, 0xf31f7e4888888820, 0x8d1ed9513d73627a, +]; +const MUL_1_2: [u64; 8] = [ + 0xa31a6937df4f002d, 0x330286cb160d2aab, 0x2cc9cedc00898bbf, 0x11cd54a8b7b446fb, + 0x54e88b2ce997ac41, 0x681379fa1ee294f9, 0xe97935422b763368, 0x454ccf8627e24388, +]; + +const NEG_0: [u64; 8] = [ + 0xf52f2ff91a8c36fc, 0x8e7396d22c9db689, 0x8def9edc78335f82, 0xb681b3b196c66a5c, + 0xbbbef4e191a3070c, 0x5b108b787a81249b, 0x5c4dadfc22bb107d, 0x101a700cddf2f820, +]; +const INV_0: [u64; 8] = [ + 0x41ed9ee9edf1e127, 0x629b65e1983ad38d, 0x381369c6c22fc3eb, 0xf4434f36655e7b9b, + 0xc36a56c05b37b57b, 0x023e5b2381c593f8, 0x6c8d112743f7c34b, 0x9412b9a2e555f20d, +]; +const NEG_1: [u64; 8] = [ + 0x730a1f9c6ea51b5a, 0x9b39d73c557acb2f, 0xb829944a759586fc, 0xb13687763d32ce9b, + 0xeda6c0db61846232, 0x9e6471a7f3ffbf09, 0xa38b0d9bad4b470a, 0x4e3670dafb82a5cd, +]; +const INV_1: [u64; 8] = [ + 0xca11b244b43e0ae8, 0xe93482a21cb9770c, 0x3f62aaac168db6a0, 0x70de41428b5d1922, + 0x5a68aac40d187bf6, 0x0a009a925a7a6031, 0x78d4a9241ff9bf94, 0x94974ce662b85e02, +]; +const NEG_2: [u64; 8] = [ + 0x59b080dd8d84dd06, 0xaa0ee71d0d3c4d6c, 0x9ceb267d52e3f4f3, 0xd5827ec60d4c6502, + 0xf75474d691717f4d, 0xf25a484d365b21c5, 0x56d5a5360209d322, 0x3077ac735d0c43bd, +]; +const INV_2: [u64; 8] = [ + 0x5d7759458b030e5c, 0x881a07b802a6ba18, 0xfda20f60de456b62, 0xeaeae6130dc42c59, + 0x8d627e9151b969ea, 0x10ba57632a83524c, 0x6dd740d37a266bff, 0x773c96b9eaa954ac, +]; + +fn fe(limbs: [u64; 8]) -> Bp512r1FieldElement { + Bp512r1FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + let pairs: [(usize, usize, [u64; 8], [u64; 8], [u64; 8]); 3] = [ + (0, 1, ADD_0_1, SUB_0_1, MUL_0_1), + (0, 2, ADD_0_2, SUB_0_2, MUL_0_2), + (1, 2, ADD_1_2, SUB_1_2, MUL_1_2), + ]; + for (i, j, a, s, m) in pairs { + assert_eq!(vals[i].add(&vals[j]), fe(a), "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), fe(a), "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), fe(s), "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), fe(m), "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), fe(m), "mul is commutative ({j},{i})"); + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2]; + let negs = [NEG_0, NEG_1, NEG_2]; + let invs = [INV_0, INV_1, INV_2]; + for i in 0..3 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + for &v in &vals { + assert_eq!(v.add(&Bp512r1FieldElement::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&Bp512r1FieldElement::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), Bp512r1FieldElement::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), Bp512r1FieldElement::ZERO, "x + (-x) == 0"); + assert_eq!(v.mul(&v.invert()), Bp512r1FieldElement::ONE, "x * x^-1 == 1"); + } + assert_eq!( + Bp512r1FieldElement::ZERO.invert(), + Bp512r1FieldElement::ZERO, + "0^-1 == 0 by convention" + ); +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..8 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + use bouncycastle_ec::bp512r1::P_LIMBS; + // p itself must reduce to 0, and p+1 to 1. + assert_eq!(fe(P_LIMBS), Bp512r1FieldElement::ZERO); + assert_eq!( + fe([ + 0x28aa6056583a48f4, 0x2881ff2f2d82c685, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b + ]), + Bp512r1FieldElement::ONE + ); + + // The maximum limb pattern, 2^512 - 1, is the widest input the single conditional + // subtraction has to cover (it is < 2p, since p > 2^511); expected value is + // 2^512 - 1 - p, computed in Python. + assert_eq!( + fe([u64::MAX; 8]), + fe([ + 0xd7559fa9a7c5b70c, 0xd77e00d0d27d397a, 0x51325ed5195c7f19, 0x82b264ff643997bd, + 0x299c63358fccf78e, 0x34cf724c4c362df1, 0xc02b1951cc3603f8, 0x5522624724163b74 + ]) + ); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the P-256 field test's property check. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 8] { + [ + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + ] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0xB512256B512256B5); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != Bp512r1FieldElement::ZERO { + assert_eq!(a.mul(&a.invert()), Bp512r1FieldElement::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE0000000007); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 8], + { + let mut l = [0u64; 8]; + l[0] = 1; + l + }, + [u64::MAX; 8], + { + let mut l = [u64::MAX; 8]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `is_zero` is the mask every exceptional-case select in this crate's point arithmetic keys +/// off (infinity detection, the `H == 0` / `R == 0` same-point and opposite-point cases), and +/// otherwise exercised only through those selects. Both truth values are pinned here, with +/// the nonzero side walked across every limb position so a mask that inspected only some limbs +/// would be caught. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(Bp512r1FieldElement::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::bp512r1::P_LIMBS).is_zero().to_bool(), "p reduces to 0"); + assert!(!Bp512r1FieldElement::ONE.is_zero().to_bool()); + for limb_idx in 0..8 { + let mut limbs = [0u64; 8]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `add`'s carry-out correction and `sub`'s borrow correction, pinned at the operands that force +/// them: `(p-1) + (p-1)` is the largest sum two canonical elements can form, and `0 - 1` the +/// smallest difference. The expected values are `p - 2` and `p - 1` themselves (computed from +/// `p` in Python), so this is a check of the wrap-around handling, not of the digits of `p`. +#[test] +fn known_answer_add_carry_and_sub_borrow_at_the_extremes() { + let p_minus_1 = fe([ + 0x28aa6056583a48f2, 0x2881ff2f2d82c685, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ]); + let p_minus_2 = fe([ + 0x28aa6056583a48f1, 0x2881ff2f2d82c685, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ]); + let two = fe([ + 0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ]); + + assert_eq!(p_minus_1.add(&p_minus_1), p_minus_2, "(p-1) + (p-1) == p-2"); + assert_eq!( + p_minus_1.add(&Bp512r1FieldElement::ONE), + Bp512r1FieldElement::ZERO, + "(p-1) + 1 == 0" + ); + assert_eq!(Bp512r1FieldElement::ZERO.sub(&Bp512r1FieldElement::ONE), p_minus_1, "0 - 1 == p-1"); + assert_eq!(Bp512r1FieldElement::ONE.sub(&p_minus_1), two, "1 - (p-1) == 2"); + assert_eq!(p_minus_1.negate(), Bp512r1FieldElement::ONE, "-(p-1) == 1"); + assert_eq!(Bp512r1FieldElement::ONE.invert(), Bp512r1FieldElement::ONE, "1^-1 == 1"); + assert_eq!(p_minus_1.invert(), p_minus_1, "(p-1)^-1 == p-1, since (p-1)^2 == 1"); +} + +/// Products at the reduction's extremes rather than in the middle of its range, the same class +/// of check `p256_field_tests.rs`/`p384_field_tests.rs`/`sm2_field_tests.rs` already carry and +/// this curve did not: `(p-1)^2` and `(p-1)(p-2)` (the largest products two canonical elements +/// can form), operands within `2^64` of `p`, and operands with the top bit set. Expected values +/// are Python's `(a * b) % p`, computed independently of this crate's arithmetic +/// (`random.seed(20260918)` for the pseudorandom operands). +#[test] +fn known_answer_mul_at_the_reduction_bounds() { + let cases: [([u64; 8], [u64; 8], [u64; 8]); 7] = [ + // (p-1)^2 == 1 + ( + [ + 0x28aa6056583a48f2, 0x2881ff2f2d82c685, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ], + [ + 0x28aa6056583a48f2, 0x2881ff2f2d82c685, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ], + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + ), + // (p-1)(p-2) == 2 + ( + [ + 0x28aa6056583a48f2, 0x2881ff2f2d82c685, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ], + [ + 0x28aa6056583a48f1, 0x2881ff2f2d82c685, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ], + [ + 0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + ), + // (p-1) * 2^(bits-1) == p - 2^(bits-1) + ( + [ + 0x28aa6056583a48f2, 0x2881ff2f2d82c685, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ], + [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x8000000000000000, + ], + [ + 0x28aa6056583a48f3, 0x2881ff2f2d82c685, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0x2add9db8dbe9c48b, + ], + ), + // both operands within 2^64 of p + ( + [ + 0x1b159ef51f1c31ba, 0x2881ff2f2d82c685, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ], + [ + 0x4133222820236744, 0x2881ff2f2d82c684, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ], + [ + 0xb1071c91f4e0f8f7, 0x0c478df11945c04d, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + ), + // both operands within 2^64 of p + ( + [ + 0x62ac133e50395289, 0x2881ff2f2d82c684, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ], + [ + 0x623bcfdeff7791a6, 0x2881ff2f2d82c684, 0xaecda12ae6a380e6, 0x7d4d9b009bc66842, + 0xd6639cca70330871, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ], + [ + 0x2629eea844c3e3e2, 0x997832a10fbd8ca7, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ], + ), + // both operands with the top bit set + ( + [ + 0xbe535d1034b561c4, 0xa87a664d92a30a7c, 0x377af0518c372858, 0x88e45984ca23e7e7, + 0x24d89be08e24a733, 0x114eb63a3f93f12e, 0x0dfe4515f0c4913d, 0x910e948373cd463e, + ], + [ + 0x531ae1b5518b6f90, 0x8610dc484a75b54f, 0x77614f1c416a4a75, 0x94a81f1a876316be, + 0x50babfc04b10be47, 0x5eb5adf69baf73cb, 0x545c70351b88e1b3, 0x918329026e50138d, + ], + [ + 0x5ed4e9b6c60565dd, 0x17f28073cf6b3d64, 0x7276b4974f2527c1, 0x47a893403c53bd46, + 0xc7b0ae0b687b05b9, 0x88d85354bf92319a, 0xfba4bb8e9eba49cb, 0x92822cb5a91351a7, + ], + ), + // both operands with the top bit set + ( + [ + 0x3321dbf084e48e4f, 0x145b706f1b5e50db, 0x712cccef3ba46161, 0xa8ffb21b1b668159, + 0x1962770441c58300, 0x29cf9eb19ee826bd, 0xa9f908bb470d2e17, 0xa8f73debe3d87f40, + ], + [ + 0xaf588834885477cd, 0x76d4b6faa2e5a4b7, 0x7796e22eda5368f9, 0xf6b6487a2ecee85d, + 0xb348365113507314, 0xf8188df8d2f6ffd8, 0x28f96ea5a33fc277, 0x9bb18fdddce4f62e, + ], + [ + 0xba5c5f288de9dea9, 0x10bc414bec68ac72, 0x6a393e92f54abad0, 0x1422fce8c87fa1f5, + 0x82b31aaf3bb909b6, 0xf6d57d61c836fc23, 0x25ad448d71a4dfce, 0x2c58b753d0cb00f0, + ], + ), + ]; + for (a, b, expected) in cases { + assert_eq!(fe(a).mul(&fe(b)), fe(expected), "a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(b).mul(&fe(a)), fe(expected), "commuted: a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(a).square(), fe(a).mul(&fe(a)), "square at the same extreme: a = {a:x?}"); + } +} diff --git a/crypto/ec/tests/bp512r1_mul_tests.rs b/crypto/ec/tests/bp512r1_mul_tests.rs new file mode 100644 index 000000000..db3754e31 --- /dev/null +++ b/crypto/ec/tests/bp512r1_mul_tests.rs @@ -0,0 +1,172 @@ +//! Behavioural tests for [`multiply_point`] (`[k]Q`, constant time in `k`): cross-checked against +//! the fixed-base comb for `Q = G`, against the variable-time wNAF multiplier for a random `Q`, +//! and against `[a k mod n]G` for `Q = [a]G` (three multipliers of independent design computing +//! the same point), plus the edge cases a windowed multiplier can get wrong: `k = 0`, `k = 1`, +//! `k = n-1`, a window of all ones, `Q` the identity, `Q` negated, and `Q` given in a +//! non-canonical-`Z` Jacobian representation. + +use bouncycastle_ec::bp512r1::Bp512r1FieldElement; +use bouncycastle_ec::bp512r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp512r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp512r1_mul::multiply_point; +use bouncycastle_ec::bp512r1_point::Bp512r1JacobianPoint; +use bouncycastle_ec::bp512r1_scalar::{ + Bp512r1PublicScalar, Bp512r1Scalar, Bp512r1ScalarField, N_LIMBS, +}; +use bouncycastle_ec::bp512r1_wnaf::shamir_multiply; +use bouncycastle_ec::nat; + +const LIMBS: usize = 8; + +fn g() -> Bp512r1JacobianPoint { + Bp512r1JacobianPoint::from_affine( + Bp512r1FieldElement::from_limbs(G_X_LIMBS), + Bp512r1FieldElement::from_limbs(G_Y_LIMBS), + ) +} + +fn zero() -> [u64; LIMBS] { + [0; LIMBS] +} + +fn small(v: u64) -> [u64; LIMBS] { + let mut limbs = zero(); + limbs[0] = v; + limbs +} + +fn one() -> [u64; LIMBS] { + small(1) +} + +fn n_minus_1() -> [u64; LIMBS] { + let (limbs, borrow) = nat::sub(&N_LIMBS, &one()); + assert_eq!(borrow, 0); + limbs +} + +/// A pseudorandom value reduced into `[0, n)`, so it is a valid scalar for every multiplier. +fn reduced(limbs: [u64; LIMBS]) -> [u64; LIMBS] { + Bp512r1ScalarField::from_limbs(limbs).to_limbs() +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; LIMBS] { + let mut limbs = zero(); + for limb in limbs.iter_mut() { + *limb = self.next_u64(); + } + limbs + } +} + +#[test] +fn k_times_g_agrees_with_the_comb_multiplier() { + let mut rng = Xorshift64(0x9E3779B97F4A7C15); + let mut cases = vec![one(), small(2), small(3), small(15), small(16), small(17)]; + cases.push(n_minus_1()); + let mut n_minus_2 = n_minus_1(); + n_minus_2[0] -= 1; + cases.push(n_minus_2); + for _ in 0..50 { + cases.push(reduced(rng.next_limbs())); + } + for k in cases { + let via_mul = multiply_point(&Bp512r1Scalar::from_limbs(k), &g()); + let via_comb = comb_multiply_base_point(&Bp512r1Scalar::from_limbs(k)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "k = {k:x?}"); + } +} + +#[test] +fn k_times_q_agrees_with_the_wnaf_multiplier_and_with_the_scalar_field() { + let mut rng = Xorshift64(0xC2B2AE3D27D4EB4F); + for _ in 0..50 { + let a = reduced(rng.next_limbs()); + let k = reduced(rng.next_limbs()); + let q = comb_multiply_base_point(&Bp512r1Scalar::from_limbs(a)); + + let via_mul = multiply_point(&Bp512r1Scalar::from_limbs(k), &q); + + // [0]G + [k]Q by the variable-time multiplier. + let via_wnaf = shamir_multiply( + &Bp512r1PublicScalar::from_limbs(zero()), + &Bp512r1PublicScalar::from_limbs(k), + &q, + ); + assert_eq!(via_mul.to_affine(), via_wnaf.to_affine(), "wnaf: a = {a:x?}, k = {k:x?}"); + + // [a k mod n]G by the fixed-base multiplier. + let ak = + Bp512r1ScalarField::from_limbs(a).mul(&Bp512r1ScalarField::from_limbs(k)).to_limbs(); + let via_comb = comb_multiply_base_point(&Bp512r1Scalar::from_limbs(ak)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "comb: a = {a:x?}, k = {k:x?}"); + } +} + +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let q = g().double(); + assert!(multiply_point(&Bp512r1Scalar::from_limbs(zero()), &q).is_infinity().to_bool()); +} + +#[test] +fn edge_case_k_equals_one_is_q() { + let q = g().double(); + assert_eq!(multiply_point(&Bp512r1Scalar::from_limbs(one()), &q).to_affine(), q.to_affine()); +} + +#[test] +fn edge_case_k_equals_n_minus_1_is_minus_q() { + let q = g().double().add(&g()); + let result = multiply_point(&Bp512r1Scalar::from_limbs(n_minus_1()), &q); + assert_eq!(result.to_affine(), q.negate().to_affine()); +} + +#[test] +fn edge_case_q_is_infinity() { + let k = reduced(Xorshift64(0x2545F4914F6CDD1D).next_limbs()); + assert!( + multiply_point(&Bp512r1Scalar::from_limbs(k), &Bp512r1JacobianPoint::INFINITY) + .is_infinity() + .to_bool() + ); +} + +#[test] +fn negating_q_negates_the_result() { + let k = reduced(Xorshift64(0x1234_5678_9ABC_DEF1).next_limbs()); + let q = g().double(); + let kq = multiply_point(&Bp512r1Scalar::from_limbs(k), &q); + let k_neg_q = multiply_point(&Bp512r1Scalar::from_limbs(k), &q.negate()); + assert_eq!(k_neg_q.to_affine(), kq.negate().to_affine()); +} + +#[test] +fn q_in_a_non_canonical_z_representation_gives_the_same_result() { + // (X, Y, Z) and (λ²X, λ³Y, λZ) are the same affine point; the multiplier must treat them alike. + let k = reduced(Xorshift64(0xDEADBEEFCAFEF00D).next_limbs()); + let q = g().double(); + let lambda = Bp512r1FieldElement::from_limbs(small(7)); + let lambda2 = lambda.mul(&lambda); + let lambda3 = lambda2.mul(&lambda); + let scaled = + Bp512r1JacobianPoint { x: q.x.mul(&lambda2), y: q.y.mul(&lambda3), z: q.z.mul(&lambda) }; + assert_eq!(scaled.to_affine(), q.to_affine(), "the scaled point is the same point"); + assert_eq!( + multiply_point(&Bp512r1Scalar::from_limbs(k), &scaled).to_affine(), + multiply_point(&Bp512r1Scalar::from_limbs(k), &q).to_affine() + ); +} diff --git a/crypto/ec/tests/bp512r1_point_tests.rs b/crypto/ec/tests/bp512r1_point_tests.rs new file mode 100644 index 000000000..2de67d033 --- /dev/null +++ b/crypto/ec/tests/bp512r1_point_tests.rs @@ -0,0 +1,115 @@ +//! Known-answer tests for [`Bp512r1JacobianPoint`] arithmetic. +//! +//! `G`, `2G`, `3G` were computed independently in Python via the standard affine group law +//! (general `a`), not from this crate, and cross-checked against `n*G == infinity`: +//! +//! ```text +//! p = 0xAADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA703308717D4D9B009BC66842AECDA12AE6A380E62881FF2F2D82C68528AA6056583A48F3 +//! a = 0x7830A3318B603B89E2327145AC234CC594CBDD8D3DF91610A83441CAEA9863BC2DED5D5AA8253AA10A2EF1C98B9AC8B57F1117A72BF2C7B9E7C1AC4D77FC94CA +//! # affine add/double via the standard general-a formulas +//! ``` + +use bouncycastle_ec::bp512r1::Bp512r1FieldElement; +use bouncycastle_ec::bp512r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp512r1_point::Bp512r1JacobianPoint; + +const TWO_G_X: [u64; 8] = [ + 0x080447def02f4850, 0xf698f404089a4cc5, 0x97116e702915a4f4, 0x957877f3a8f0f725, + 0xa30d3035f4cb6581, 0xd18d8141b8a18064, 0x0a63285758f399b3, 0x9f4945f680edf980, +]; +const TWO_G_Y: [u64; 8] = [ + 0xe269ad21be592e71, 0x63a42e4181fd929c, 0xa5c3b602b0960cbf, 0xfdf99bb1709c700f, + 0x5a2c366b03e5d1b2, 0x149ce1238d3f1e0f, 0x49826b716292f29d, 0x6d6b4b188b699c56, +]; +const THREE_G_X: [u64; 8] = [ + 0x5d4abf882ccb8d94, 0xf56e34abfa9ac720, 0x4780ae53e1853d62, 0xc1f6fb975ceecade, + 0x09c09cefd830151b, 0x907c80ef3bc24593, 0x36cdd42543f20afe, 0x08dd87e12b0a4cc4, +]; +const THREE_G_Y: [u64; 8] = [ + 0x5cdeafba05b02c37, 0x8cce5bc75d8de649, 0xfebfcb69c0f37c5f, 0xacdab8eb772327b3, + 0xba0b382e1716d843, 0x43d903b4a6334c4b, 0x756ff0067376fa75, 0x026ef5c6e1dab71d, +]; + +fn fe(limbs: [u64; 8]) -> Bp512r1FieldElement { + Bp512r1FieldElement::from_limbs(limbs) +} + +fn g() -> Bp512r1JacobianPoint { + Bp512r1JacobianPoint::from_affine(fe(G_X_LIMBS), fe(G_Y_LIMBS)) +} + +#[test] +fn known_answer_double() { + let doubled = g().double(); + let (x, y) = doubled.to_affine().unwrap(); + assert_eq!(x, fe(TWO_G_X)); + assert_eq!(y, fe(TWO_G_Y)); +} + +#[test] +fn known_answer_add() { + let sum = g().add(&g().double()); + let (x, y) = sum.to_affine().unwrap(); + assert_eq!(x, fe(THREE_G_X)); + assert_eq!(y, fe(THREE_G_Y)); +} + +#[test] +fn point_plus_negation_is_infinity() { + let g = g(); + assert!(g.add(&g.negate()).is_infinity().to_bool()); +} + +#[test] +fn infinity_identities() { + let g = g(); + let infinity = Bp512r1JacobianPoint::INFINITY; + assert!(infinity.is_infinity().to_bool()); + assert!(infinity.to_affine().is_none()); + + let (x1, y1) = g.add(&infinity).to_affine().unwrap(); + let (x2, y2) = g.to_affine().unwrap(); + assert_eq!((x1, y1), (x2, y2), "P + infinity == P"); + + let (x3, y3) = infinity.add(&g).to_affine().unwrap(); + assert_eq!((x3, y3), (x2, y2), "infinity + P == P"); + + assert!(infinity.add(&infinity).is_infinity().to_bool()); +} + +#[test] +fn scaled_z_representation_still_adds_correctly() { + // (X, Y, Z) and (X*c^2, Y*c^3, Z*c) represent the same affine point for any nonzero c; the + // branch-free add/double formulas must agree regardless of which representative is used. + let c = fe([7, 0, 0, 0, 0, 0, 0, 0]); + let c2 = c.mul(&c); + let c3 = c2.mul(&c); + let g = g(); + let scaled = Bp512r1JacobianPoint { x: g.x.mul(&c2), y: g.y.mul(&c3), z: g.z.mul(&c) }; + + let canonical_double = g.double().to_affine().unwrap(); + let scaled_double = scaled.double().to_affine().unwrap(); + assert_eq!(canonical_double, scaled_double); + + let canonical_sum = g.add(&g.double()).to_affine().unwrap(); + let scaled_sum = scaled.add(&g.double()).to_affine().unwrap(); + assert_eq!(canonical_sum, scaled_sum); +} + +/// `is_infinity` was previously only ever asserted true. The false side is pinned on the base +/// point and on values derived from it, and doubling the identity must stay at the identity +/// (`double` is total, per its docs, and the comb multiplier's accumulator starts there). +#[test] +fn is_infinity_is_false_for_finite_points_and_infinity_doubles_to_itself() { + let g = g(); + assert!(!g.is_infinity().to_bool(), "G is not the identity"); + assert!(!g.double().is_infinity().to_bool(), "2G is not the identity"); + assert!(!g.negate().is_infinity().to_bool(), "-G is not the identity"); + assert!(!g.add(&g.double()).is_infinity().to_bool(), "3G is not the identity"); + + let inf = Bp512r1JacobianPoint::INFINITY; + assert!(inf.double().is_infinity().to_bool(), "2 * infinity == infinity"); + assert!(inf.double().to_affine().is_none()); + assert!(inf.negate().is_infinity().to_bool(), "-infinity == infinity"); + assert!(inf.double().add(&g).to_affine().is_some(), "2 * infinity + G == G, a finite point"); +} diff --git a/crypto/ec/tests/bp512r1_scalar_tests.rs b/crypto/ec/tests/bp512r1_scalar_tests.rs new file mode 100644 index 000000000..07ad96ac4 --- /dev/null +++ b/crypto/ec/tests/bp512r1_scalar_tests.rs @@ -0,0 +1,624 @@ +//! Known-answer tests for [`Bp512r1ScalarField`] arithmetic mod the brainpoolP512r1 curve +//! order `n`. Expected values computed independently in Python: +//! +//! ```text +//! n = 0xaadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca70330870553e5c414ca92619418661197fac10471db1d381085ddaddb58796829ca90069 +//! random.seed(830512) +//! vals = [random.randrange(1, n) for _ in range(5)] + [0] +//! # for each pair (i, j) with i < j: (vals[i]+vals[j]) % n, (vals[i]-vals[j]) % n, (vals[i]*vals[j]) % n +//! # for each i: (-vals[i]) % n, pow(vals[i], n-2, n) if vals[i] != 0 else 0 +//! ``` + +use bouncycastle_ec::bp512r1_scalar::{ + Bp512r1PublicScalar, Bp512r1Scalar, Bp512r1ScalarField, N_LIMBS, +}; + +const VALS_0: [u64; 8] = [ + 0xfe0a77c2a06c4222, 0xfc97dd8974930a3c, 0xbe7a0837a055f066, 0x5aabde57ce7eeb85, + 0x8528df7738313dc2, 0x2e2c027dd4f86ed6, 0x0055d5619a9a9691, 0x69b3c457f3d5201e, +]; +const VALS_1: [u64; 8] = [ + 0xf9d09068277a5920, 0x8bd21c72c1ad0315, 0x2f498000b2d67897, 0x97eb6b41f1a7fc72, + 0x0563331ae3d365ad, 0x3a2d9b815b72e631, 0xb0484c83fc6e9b74, 0x6060f39adf676da4, +]; +const VALS_2: [u64; 8] = [ + 0x0323b915f0a14117, 0x234f534c87e9cc42, 0xf6aea69a237a2ff9, 0x06f062d59ac95de5, + 0xb2c8accb4dee1b9f, 0x034f02cfe3fdf48f, 0x8b3b34575e1809c6, 0x32873537150568b5, +]; +const VALS_3: [u64; 8] = [ + 0xd76804f2f1e81b80, 0x5b1290b8d7accb6a, 0xca4cbddd4d8b743a, 0xbd754e0cb7e64fcc, + 0xb7acdd72b5cfcb01, 0x31aa1f252466b32f, 0xae3fe91854e97b8c, 0x84b2fe72174c46b9, +]; +const VALS_4: [u64; 8] = [ + 0x43c9d15ffb05bfda, 0x2060aa26716afeb8, 0x38584661d9c8a19d, 0x5e6e9225b3229aac, + 0xc17b7826b9aa9b40, 0xcb2478003de57db1, 0x8287c262f12e2847, 0x205ded5753d56738, +]; +const VALS_5: [u64; 8] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, +]; + +const ADD_0_1: [u64; 8] = [ + 0x425371a82b3d9ad9, 0x6ab8267b2de23275, 0xac3d271ed38058b7, 0x9d58ed58737dc1de, + 0xb42875c7abd19aff, 0x9d29104b7ca182f8, 0x70c93b37633f35fd, 0x1f371a39f752c937, +]; +const SUB_0_1: [u64; 8] = [ + 0x0439e75a78f1e902, 0x70c5c116b2e60727, 0x8f308836ed7f77cf, 0xc2c07315dcd6ef13, + 0x7fc5ac5c545dd814, 0xf3fe66fc798588a5, 0x500d88dd9e2bfb1c, 0x0952d0bd146db279, +]; +const MUL_0_1: [u64; 8] = [ + 0xc44d671d5b410039, 0xa2ee104f20255893, 0xd7599f2fe454d9d2, 0x2bc875c982b5ebb9, + 0x5beeab35b6a749c4, 0x971dea48a356f807, 0xe8d27dfc3e79f1db, 0x5ffdb2e752cb0ac5, +]; +const ADD_0_2: [u64; 8] = [ + 0x012e30d8910d8339, 0x1fe730d5fc7cd67f, 0xb528aed1c3d02060, 0x619c412d6948496b, + 0x37f18c42861f5961, 0x317b054db8f66366, 0x8b9109b8f8b2a057, 0x9c3af98f08da88d3, +]; +const SUB_0_2: [u64; 8] = [ + 0xfae6beacafcb010b, 0xd9488a3ceca93dfa, 0xc7cb619d7cdbc06d, 0x53bb7b8233b58d9f, + 0xd26032abea432223, 0x2adcffadf0fa7a46, 0x751aa10a3c828ccb, 0x372c8f20decfb768, +]; +const MUL_0_2: [u64; 8] = [ + 0x8180ae76f33246ac, 0x8f7feda2a40391c5, 0xbfe1555f8b512431, 0x7e19d29f54c55c94, + 0xf2bf9f3e117cc4e4, 0xccf58e4f6efe0413, 0xe527a360b681c55a, 0x92e7e2047d478b7f, +]; +const ADD_0_3: [u64; 8] = [ + 0x1feae632f5ab5d39, 0x39f89ac143e1faca, 0x474064fb6e35545a, 0xc2e2d02339bc1539, + 0x6672201f7dce0053, 0x94a593ef45954ff7, 0x6ec0d7cbbbba1615, 0x438925112f37a24c, +]; +const SUB_0_3: [u64; 8] = [ + 0xdc2a09524b2d270b, 0xbf372051a54419af, 0x35b3ab73d2768c73, 0xf274ec8c6341c1d2, + 0xa3df9ecef2947b30, 0xc7b2710c645b8db5, 0x91ead2f7797b170c, 0x8fde639eb8729def, +]; +const MUL_0_3: [u64; 8] = [ + 0xf17d8127c96aa632, 0x6dea1a537962aec1, 0x1bb094dfadbe777e, 0x2604a8dad54e6a41, + 0x6b138d7203451d2c, 0x42b8ead58f7850c3, 0xe1c9170567fde334, 0x7c8b11d749303c98, +]; +const ADD_0_4: [u64; 8] = [ + 0x41d449229b7201fc, 0x1cf887afe5fe08f5, 0xf6d24e997a1e9204, 0xb91a707d81a18631, + 0x46a4579df1dbd902, 0xf9507a7e12ddec88, 0x82dd97c48bc8bed8, 0x8a11b1af47aa8756, +]; +const SUB_0_4: [u64; 8] = [ + 0xba40a662a5668248, 0xdc37336303280b84, 0x8621c1d5c68d4ec9, 0xfc3d4c321b5c50d9, + 0xc3ad67507e86a281, 0x63078a7d9712f124, 0x7dce12fea96c6e49, 0x4955d7009fffb8e5, +]; +const MUL_0_4: [u64; 8] = [ + 0x478aff9e19b22d0e, 0x2a204c4536c2f0db, 0x84c23a2f2c17a4be, 0xc445536b2d8884e8, + 0xd210ea515d10cdc2, 0xcf1616d9d9dc8259, 0x16862a6e82ff3566, 0x906228935627ebb4, +]; +const ADD_0_5: [u64; 8] = [ + 0xfe0a77c2a06c4222, 0xfc97dd8974930a3c, 0xbe7a0837a055f066, 0x5aabde57ce7eeb85, + 0x8528df7738313dc2, 0x2e2c027dd4f86ed6, 0x0055d5619a9a9691, 0x69b3c457f3d5201e, +]; +const SUB_0_5: [u64; 8] = [ + 0xfe0a77c2a06c4222, 0xfc97dd8974930a3c, 0xbe7a0837a055f066, 0x5aabde57ce7eeb85, + 0x8528df7738313dc2, 0x2e2c027dd4f86ed6, 0x0055d5619a9a9691, 0x69b3c457f3d5201e, +]; +const MUL_0_5: [u64; 8] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, +]; +const ADD_1_2: [u64; 8] = [ + 0xfcf4497e181b9a37, 0xaf216fbf4996cf57, 0x25f8269ad650a890, 0x9edbce178c715a58, + 0xb82bdfe631c1814c, 0x3d7c9e513f70dac0, 0x3b8380db5a86a53a, 0x92e828d1f46cd65a, +]; +const SUB_1_2: [u64; 8] = [ + 0xf6acd75236d91809, 0x6882c92639c336d3, 0x389ad9668f5c489e, 0x90fb086c56de9e8c, + 0x529a864f95e54a0e, 0x36de98b17774f1a1, 0x250d182c9e5691ae, 0x2dd9be63ca6204ef, +]; +const MUL_1_2: [u64; 8] = [ + 0x6845eed377e37a21, 0xf41e07ef4414cad5, 0x046b3d96efb2d6c2, 0xf1197444817e68a6, + 0x3b7307b86fd9d64f, 0x470a947da9794c7b, 0x64a1cc6cd8d606e9, 0x1c2f97c872041289, +]; +const ADD_1_3: [u64; 8] = [ + 0x1bb0fed87cb97437, 0xc932d9aa90fbf3a3, 0xb80fdcc480b5dc8a, 0x00225d0d5ce52625, + 0xe6ac73c32970283f, 0xa0a72cf2cc0fc751, 0x1eb34eee1d8e1af8, 0x3a3654541ac9efd3, +]; +const SUB_1_3: [u64; 8] = [ + 0xd7f021f7d23b3e09, 0x4e715f3af25e1288, 0xa683233ce4f714a4, 0x2fb47976866ad2be, + 0x2419f2729e36a31c, 0xd3b40a0fead60510, 0x41dd4a19db4f1bef, 0x868b92e1a404eb76, +]; +const MUL_1_3: [u64; 8] = [ + 0xbb8ee703b432dfe3, 0x2502d4dd4282d4bb, 0x713006f11083811a, 0x81c1905cbc10da5d, + 0x8d459675929e9106, 0x2580ad27e06fc742, 0x713f180420ed013d, 0x187e94edabfa2b3f, +]; +const ADD_1_4: [u64; 8] = [ + 0x3d9a61c8228018fa, 0xac32c699331801ce, 0x67a1c6628c9f1a34, 0xf659fd67a4ca971e, + 0xc6deab419d7e00ed, 0x05521381995863e2, 0x32d00ee6ed9cc3bc, 0x80bee0f2333cd4dd, +]; +const SUB_1_4: [u64; 8] = [ + 0xb606bf082c749946, 0x6b71724c5042045d, 0xf6f1399ed90dd6fa, 0x397cd91c3e8561c5, + 0x43e7baf42a28ca6d, 0x6f0923811d8d687f, 0x2dc08a210b40732c, 0x400306438b92066c, +]; +const MUL_1_4: [u64; 8] = [ + 0x0b22faf1ed9a838c, 0xc9c0d1f9686b8229, 0xb26912c687eef4b3, 0x3a9c1083c845fdbc, + 0xcbfde3e01746c306, 0x495ecb7619d0ff32, 0x7acf5e29e34b486b, 0x545373c3a56cb77a, +]; +const ADD_1_5: [u64; 8] = [ + 0xf9d09068277a5920, 0x8bd21c72c1ad0315, 0x2f498000b2d67897, 0x97eb6b41f1a7fc72, + 0x0563331ae3d365ad, 0x3a2d9b815b72e631, 0xb0484c83fc6e9b74, 0x6060f39adf676da4, +]; +const SUB_1_5: [u64; 8] = [ + 0xf9d09068277a5920, 0x8bd21c72c1ad0315, 0x2f498000b2d67897, 0x97eb6b41f1a7fc72, + 0x0563331ae3d365ad, 0x3a2d9b815b72e631, 0xb0484c83fc6e9b74, 0x6060f39adf676da4, +]; +const MUL_1_5: [u64; 8] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, +]; +const ADD_2_3: [u64; 8] = [ + 0x2504278645e05c2e, 0x60b010845738bccf, 0x7f75035df15993ec, 0x6f2754a106068799, + 0x9411ed73938ade30, 0x69c89441549ad5b0, 0xf9a636c17f37894a, 0x0c5c95f05067eae3, +]; +const SUB_2_3: [u64; 8] = [ + 0xe1434aa59b622600, 0xe5ee9614b89adbb4, 0x6de849d6559acc05, 0x9eb9710a2f8c3432, + 0xd17f6c230851590d, 0x9cd5715e7361136e, 0x1cd031ed3cf88a41, 0x58b1d47dd9a2e687, +]; +const MUL_2_3: [u64; 8] = [ + 0x14f86b797c43eede, 0x5c254f9c1cc5d82b, 0xa3d47228f954a4a2, 0xb22ff597a11f3a8c, + 0x96a5ac0fafc8f2a2, 0xf077bb2f3a2b4ef4, 0xe231cb40916b0c59, 0x1098e308ae2103ef, +]; +const ADD_2_4: [u64; 8] = [ + 0x46ed8a75eba700f1, 0x43affd72f954cafa, 0x2f06ecfbfd42d196, 0x655ef4fb4debf892, + 0x744424f20798b6df, 0xce737ad021e37241, 0x0dc2f6ba4f46320d, 0x52e5228e68dacfee, +]; +const SUB_2_4: [u64; 8] = [ + 0xbf59e7b5f59b813d, 0x02eea926167ecd89, 0xbe56603849b18e5c, 0xa881d0afe7a6c339, + 0xf14d34a49443805e, 0x382a8acfa61876dd, 0x08b371f46ce9e17e, 0x122947dfc130017d, +]; +const MUL_2_4: [u64; 8] = [ + 0x5c49ff018d923f27, 0x6e0712f807fcafd5, 0xff29918a7c7af2df, 0x559b5b32a2562215, + 0x75d30700543df145, 0xe195bbe82ae60c2d, 0xc4795323d3898989, 0x243ca49474e5bbb8, +]; +const ADD_2_5: [u64; 8] = [ + 0x0323b915f0a14117, 0x234f534c87e9cc42, 0xf6aea69a237a2ff9, 0x06f062d59ac95de5, + 0xb2c8accb4dee1b9f, 0x034f02cfe3fdf48f, 0x8b3b34575e1809c6, 0x32873537150568b5, +]; +const SUB_2_5: [u64; 8] = [ + 0x0323b915f0a14117, 0x234f534c87e9cc42, 0xf6aea69a237a2ff9, 0x06f062d59ac95de5, + 0xb2c8accb4dee1b9f, 0x034f02cfe3fdf48f, 0x8b3b34575e1809c6, 0x32873537150568b5, +]; +const MUL_2_5: [u64; 8] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, +]; +const ADD_3_4: [u64; 8] = [ + 0x1b31d652eceddb5a, 0x7b733adf4917ca23, 0x02a5043f275415d7, 0x1be3e0326b08ea79, + 0x792855996f7a6642, 0xfcce9725624c30e1, 0x30c7ab7b4617a3d3, 0xa510ebc96b21adf2, +]; +const SUB_3_4: [u64; 8] = [ + 0x939e3392f6e25ba6, 0x3ab1e6926641ccb2, 0x91f4777b73c2d29d, 0x5f06bbe704c3b520, + 0xf631654bfc252fc1, 0x6685a724e681357d, 0x2bb826b563bb5344, 0x6455111ac376df81, +]; +const MUL_3_4: [u64; 8] = [ + 0x2d530772ccc014ca, 0x14d205116d45f4eb, 0x45f3323ee5d7c2cb, 0xb5c58fc93d3f16b7, + 0xec8b52c106a193c8, 0x686c87342340a269, 0xfddd204258f3a319, 0x0007cd0826e5e07b, +]; +const ADD_3_5: [u64; 8] = [ + 0xd76804f2f1e81b80, 0x5b1290b8d7accb6a, 0xca4cbddd4d8b743a, 0xbd754e0cb7e64fcc, + 0xb7acdd72b5cfcb01, 0x31aa1f252466b32f, 0xae3fe91854e97b8c, 0x84b2fe72174c46b9, +]; +const SUB_3_5: [u64; 8] = [ + 0xd76804f2f1e81b80, 0x5b1290b8d7accb6a, 0xca4cbddd4d8b743a, 0xbd754e0cb7e64fcc, + 0xb7acdd72b5cfcb01, 0x31aa1f252466b32f, 0xae3fe91854e97b8c, 0x84b2fe72174c46b9, +]; +const MUL_3_5: [u64; 8] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, +]; +const ADD_4_5: [u64; 8] = [ + 0x43c9d15ffb05bfda, 0x2060aa26716afeb8, 0x38584661d9c8a19d, 0x5e6e9225b3229aac, + 0xc17b7826b9aa9b40, 0xcb2478003de57db1, 0x8287c262f12e2847, 0x205ded5753d56738, +]; +const SUB_4_5: [u64; 8] = [ + 0x43c9d15ffb05bfda, 0x2060aa26716afeb8, 0x38584661d9c8a19d, 0x5e6e9225b3229aac, + 0xc17b7826b9aa9b40, 0xcb2478003de57db1, 0x8287c262f12e2847, 0x205ded5753d56738, +]; +const MUL_4_5: [u64; 8] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, +]; + +const NEG_0: [u64; 8] = [ + 0xb77d1ebffc3cbe47, 0x2119f5f793cad0a0, 0x830c58e1df561fe0, 0xfa927de97e2a3a93, + 0x513abd533801caad, 0x9d048b35ded16338, 0x3f7f114c992f6576, 0x4129d960e814a46d, +]; +const INV_0: [u64; 8] = [ + 0x4888528c419d8136, 0x2e38aed6b7ddfc2c, 0xa3cdc7065e7fe91f, 0x8947feb85a876491, + 0x7a162a6b60cd70c6, 0xb3346c8750e24000, 0x3f7315b921b97fa4, 0x5c1238c85f010c93, +]; +const NEG_1: [u64; 8] = [ + 0xbbb7061a752ea749, 0x91dfb70e46b0d7c7, 0x123ce118ccd597af, 0xbd52f0ff5b0129a7, + 0xd10069af8c5fa2c2, 0x9102f2325856ebdd, 0x8f8c9a2a375b6093, 0x4a7caa1dfc8256e6, +]; +const INV_1: [u64; 8] = [ + 0xd9fc225e3d64eb64, 0x568d31f24cf0613c, 0x1fb09034e0680459, 0xa6323d11c8344562, + 0x93fc50d18b72839f, 0x6c5b909740271afe, 0x21f0c45f3fe09a3c, 0x72d4e2b8688faccc, +]; +const NEG_2: [u64; 8] = [ + 0xb263dd6cac07bf52, 0xfa62803480740e9b, 0x4ad7ba7f5c31e04d, 0x4e4df96bb1dfc833, + 0x239aefff2244ecd1, 0xc7e18ae3cfcbdd7f, 0xb499b256d5b1f241, 0x78566881c6e45bd5, +]; +const INV_2: [u64; 8] = [ + 0xa7a0aaaf9c3338b0, 0xed1dffa3e22564de, 0xacda55f15a91ba74, 0x13d6f903015777c2, + 0x25d7548e679c3b0c, 0x498ecce06da075f1, 0xb1cd41f93f3fecc5, 0x419bd9cfae0f0263, +]; +const NEG_3: [u64; 8] = [ + 0xde1f918faac0e4e9, 0xc29f42c830b10f72, 0x7739a33c32209c0c, 0x97c90e3494c2d64c, + 0x1eb6bf57ba633d6e, 0x99866e8e8f631edf, 0x9194fd95dee0807b, 0x262a9f46c49d7dd1, +]; +const INV_3: [u64; 8] = [ + 0xef71511cf959f418, 0x6b63b4320ef080a5, 0x72c9abfe5b88e25b, 0xa0942c73caf1ce92, + 0x03dacc2c6e03b48f, 0xd97b6fc2334e23d8, 0x68a1965566ce906b, 0x827d87eab47bbb4d, +]; +const NEG_4: [u64; 8] = [ + 0x71bdc522a1a3408f, 0xfd51295a96f2dc25, 0x092e1ab7a5e36ea9, 0xf6cfca1b99868b6d, + 0x14e824a3b6886d2f, 0x000c15b375e4545d, 0xbd4d244b429bd3c0, 0x8a7fb06188145d52, +]; +const INV_4: [u64; 8] = [ + 0xa288e451c768d4fc, 0xbb5c0754e0912954, 0x1517c339f229a54a, 0x4efb46a5834f30b5, + 0x4f2aa0e3e3ec32da, 0x390309223a05b860, 0x536e0da9977db4e5, 0x598c8870a9711616, +]; +const NEG_5: [u64; 8] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, +]; +const INV_5: [u64; 8] = [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, +]; + +fn fe(limbs: [u64; 8]) -> Bp512r1ScalarField { + Bp512r1ScalarField::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5].map(fe); + let add: Vec> = vec![ + vec![ADD_0_1, ADD_0_2, ADD_0_3, ADD_0_4, ADD_0_5], + vec![ADD_1_2, ADD_1_3, ADD_1_4, ADD_1_5], + vec![ADD_2_3, ADD_2_4, ADD_2_5], + vec![ADD_3_4, ADD_3_5], + vec![ADD_4_5], + ]; + let sub: Vec> = vec![ + vec![SUB_0_1, SUB_0_2, SUB_0_3, SUB_0_4, SUB_0_5], + vec![SUB_1_2, SUB_1_3, SUB_1_4, SUB_1_5], + vec![SUB_2_3, SUB_2_4, SUB_2_5], + vec![SUB_3_4, SUB_3_5], + vec![SUB_4_5], + ]; + let mul: Vec> = vec![ + vec![MUL_0_1, MUL_0_2, MUL_0_3, MUL_0_4, MUL_0_5], + vec![MUL_1_2, MUL_1_3, MUL_1_4, MUL_1_5], + vec![MUL_2_3, MUL_2_4, MUL_2_5], + vec![MUL_3_4, MUL_3_5], + vec![MUL_4_5], + ]; + for i in 0..6 { + for j in (i + 1)..6 { + let expected_add = fe(add[i][j - i - 1]); + let expected_sub = fe(sub[i][j - i - 1]); + let expected_mul = fe(mul[i][j - i - 1]); + assert_eq!(vals[i].add(&vals[j]), expected_add, "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), expected_add, "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), expected_sub, "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), expected_mul, "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), expected_mul, "mul is commutative ({j},{i})"); + } + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5]; + let negs = [NEG_0, NEG_1, NEG_2, NEG_3, NEG_4, NEG_5]; + let invs = [INV_0, INV_1, INV_2, INV_3, INV_4, INV_5]; + for i in 0..6 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5].map(fe); + for &v in &vals { + assert_eq!(v.add(&Bp512r1ScalarField::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&Bp512r1ScalarField::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), Bp512r1ScalarField::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), Bp512r1ScalarField::ZERO, "x + (-x) == 0"); + if v != Bp512r1ScalarField::ZERO { + assert_eq!(v.mul(&v.invert()), Bp512r1ScalarField::ONE, "x * x^-1 == 1"); + } else { + assert_eq!(v.invert(), Bp512r1ScalarField::ZERO, "0^-1 == 0 by convention"); + } + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..8 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + assert_eq!(fe(N_LIMBS), Bp512r1ScalarField::ZERO); + let n_plus_1: [u64; 8] = [ + 0xb58796829ca9006a, 0x1db1d381085ddadd, 0x418661197fac1047, 0x553e5c414ca92619, + 0xd6639cca70330870, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ]; + assert_eq!(fe(n_plus_1), Bp512r1ScalarField::ONE); +} + +#[test] +fn public_scalar_round_trips_and_reduces() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + assert_eq!(Bp512r1PublicScalar::from_limbs(limbs).to_limbs(), limbs); + } + assert_eq!(Bp512r1PublicScalar::from_limbs(N_LIMBS).to_limbs(), [0, 0, 0, 0, 0, 0, 0, 0]); + let n_plus_1: [u64; 8] = [ + 0xb58796829ca9006a, 0x1db1d381085ddadd, 0x418661197fac1047, 0x553e5c414ca92619, + 0xd6639cca70330870, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ]; + assert_eq!(Bp512r1PublicScalar::from_limbs(n_plus_1).to_limbs(), [1, 0, 0, 0, 0, 0, 0, 0]); +} + +#[test] +fn public_scalar_eq_detects_a_difference_in_any_limb() { + let base = Bp512r1PublicScalar::from_limbs(VALS_0); + assert_eq!(base, Bp512r1PublicScalar::from_limbs(VALS_0)); + for limb_idx in 0..8 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!( + base, + Bp512r1PublicScalar::from_limbs(other), + "a difference in limb {limb_idx} must be detected" + ); + } +} + +#[test] +fn secret_scalar_be_bytes_round_trip() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4] { + let secret = Bp512r1Scalar::from_limbs(limbs); + let round_tripped = Bp512r1Scalar::from_be_bytes(&secret.to_be_bytes()); + assert_eq!(round_tripped, secret); + } +} + +#[test] +fn secret_scalar_from_be_bytes_reduces_out_of_range_input() { + let mut n_bytes = [0u8; 64]; + for (i, limb) in N_LIMBS.iter().rev().enumerate() { + n_bytes[i * 8..i * 8 + 8].copy_from_slice(&limb.to_be_bytes()); + } + assert_eq!( + Bp512r1Scalar::from_be_bytes(&n_bytes), + Bp512r1Scalar::from_limbs([0, 0, 0, 0, 0, 0, 0, 0]) + ); +} + +#[test] +fn scalar_field_from_secret_matches_from_limbs() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + let secret = Bp512r1Scalar::from_limbs(limbs); + assert_eq!(Bp512r1ScalarField::from_secret(&secret), fe(limbs)); + } +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property test. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 8] { + [ + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + ] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0xB512256B384256B3); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != Bp512r1ScalarField::ZERO { + assert_eq!(a.mul(&a.invert()), Bp512r1ScalarField::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE000000000F); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 8], + { + let mut l = [0u64; 8]; + l[0] = 1; + l + }, + [u64::MAX; 8], + { + let mut l = [u64::MAX; 8]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `zeroize` is the signing path's way of not leaving `d`, `k` or `k^-1` legible on the stack +/// (see the method's own docs for why this type is not wrapped in `Secret` instead). Whether the +/// volatile write survives optimization cannot be observed from Rust; that the value is actually +/// cleared can be, and is what would break if the method were ever reduced to a no-op. +#[test] +fn zeroize_clears_the_value() { + let mut a = fe([1, 2, 3, 4, 5, 6, 7, 8]); + assert_ne!(a, Bp512r1ScalarField::ZERO, "precondition: the value starts non-zero"); + a.zeroize(); + assert_eq!(a, Bp512r1ScalarField::ZERO, "zeroize must leave the value at zero"); + + // and it is idempotent, so a caller scrubbing twice on overlapping paths is harmless + a.zeroize(); + assert_eq!(a, Bp512r1ScalarField::ZERO); +} + +/// `is_zero` on the scalar field, pinned for both truth values and across every limb position +/// (see the field test of the same name for why); it is the check behind `negate`'s `0 -> 0` +/// special case. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(Bp512r1ScalarField::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::bp512r1_scalar::N_LIMBS).is_zero().to_bool(), "n reduces to 0"); + assert!(!Bp512r1ScalarField::ONE.is_zero().to_bool()); + for limb_idx in 0..8 { + let mut limbs = [0u64; 8]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `n - 1`, the largest canonical scalar, is the worst case for REDC's final conditional +/// subtraction and for `add`'s carry correction, and was absent from every known-answer set +/// above (whose values are pseudorandom, so never near `n`). Expected values follow from +/// `(n-1)^2 == 1` and `(n-1) + (n-1) == n - 2` in the field, computed from `n` in Python. +#[test] +fn known_answer_at_n_minus_1() { + let n_minus_1_limbs: [u64; 8] = [ + 0xb58796829ca90068, 0x1db1d381085ddadd, 0x418661197fac1047, 0x553e5c414ca92619, + 0xd6639cca70330870, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ]; + let n_minus_1 = fe(n_minus_1_limbs); + let n_minus_2 = fe([ + 0xb58796829ca90067, 0x1db1d381085ddadd, 0x418661197fac1047, 0x553e5c414ca92619, + 0xd6639cca70330870, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ]); + + assert_eq!(n_minus_1.to_limbs(), n_minus_1_limbs, "Montgomery round trip of n-1"); + assert_eq!(Bp512r1PublicScalar::from_limbs(n_minus_1_limbs).to_limbs(), n_minus_1_limbs); + assert_eq!(n_minus_1.add(&n_minus_1), n_minus_2, "(n-1) + (n-1) == n-2"); + assert_eq!(n_minus_1.add(&Bp512r1ScalarField::ONE), Bp512r1ScalarField::ZERO, "(n-1) + 1 == 0"); + assert_eq!(Bp512r1ScalarField::ZERO.sub(&Bp512r1ScalarField::ONE), n_minus_1, "0 - 1 == n-1"); + assert_eq!(n_minus_1.mul(&n_minus_1), Bp512r1ScalarField::ONE, "(n-1)^2 == 1"); + assert_eq!(n_minus_1.square(), Bp512r1ScalarField::ONE, "(n-1)^2 == 1, via square"); + assert_eq!(n_minus_1.negate(), Bp512r1ScalarField::ONE, "-(n-1) == 1"); + assert_eq!(n_minus_1.invert(), n_minus_1, "(n-1)^-1 == n-1"); + assert_eq!(Bp512r1ScalarField::ONE.invert(), Bp512r1ScalarField::ONE, "1^-1 == 1"); +} + +/// The variable-time inverse on the public scalar type must agree with the constant-time +/// [`Bp512r1ScalarField::invert`] everywhere: on the known-answer values (whose inverses are pinned above +/// from Python), on `0` (both return `0` by convention), `1`, `n - 1`, and on pseudorandom +/// values -- including ones with long runs of zero bits, which are where the binary algorithm +/// takes its longest halving chains. +#[test] +fn public_scalar_invert_vartime_agrees_with_constant_time_invert() { + let n_minus_1 = { + let mut l = bouncycastle_ec::bp512r1_scalar::N_LIMBS; + l[0] -= 1; + l + }; + let mut one = [0u64; 8]; + one[0] = 1; + // Values whose low limb is exactly 1 with higher limbs set: odd, so the binary algorithm must + // not halve them, yet indistinguishable from `1` by their low limb alone. A halving test that + // looked at more than the lowest bit (or at the wrong limb) breaks the `u == x1 * a` invariant + // on exactly these and nowhere the pseudorandom inputs below would reach. + let mut low_limb_one_a = [0u64; 8]; + low_limb_one_a[0] = 1; + low_limb_one_a[1] = 1; + let mut low_limb_one_b = [0u64; 8]; + low_limb_one_b[0] = 1; + low_limb_one_b[7] = 1; + let mut low_limb_one_c = [0u64; 8]; + low_limb_one_c[0] = 1; + low_limb_one_c[1] = u64::MAX; + let mut fixed: Vec<[u64; 8]> = + vec![[0u64; 8], one, n_minus_1, low_limb_one_a, low_limb_one_b, low_limb_one_c]; + fixed.extend([VALS_0, VALS_1, VALS_2]); + for limbs in fixed { + assert_eq!( + bouncycastle_ec::bp512r1_scalar::Bp512r1PublicScalar::from_limbs(limbs) + .invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } + assert_eq!( + bouncycastle_ec::bp512r1_scalar::Bp512r1PublicScalar::from_limbs([0u64; 8]) + .invert_vartime(), + Bp512r1ScalarField::ZERO, + "0^-1 == 0 by convention, matching invert" + ); + + let mut rng = Xorshift64(0x1AC0_B5EC_0000_0008); + for i in 0..500 { + let mut limbs = rng.next_limbs(); + if i % 4 == 0 { + // Sparse values: clear the low half of each limb so `u`/`v` shed many bits per step. + for limb in limbs.iter_mut() { + *limb &= 0xffff_ffff_0000_0000; + } + } + assert_eq!( + bouncycastle_ec::bp512r1_scalar::Bp512r1PublicScalar::from_limbs(limbs) + .invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } +} diff --git a/crypto/ec/tests/bp512r1_sec1_tests.rs b/crypto/ec/tests/bp512r1_sec1_tests.rs new file mode 100644 index 000000000..d89fdc001 --- /dev/null +++ b/crypto/ec/tests/bp512r1_sec1_tests.rs @@ -0,0 +1,150 @@ +//! Known-answer tests for SEC 1 §2.3.3/§2.3.4 point encoding and decoding, brainpoolP512r1. +//! +//! `G`'s coordinates are RFC 5639 §3.4's (already verified independently in +//! `bp512r1_point_tests.rs`); `2G`'s were computed independently in Python via the standard +//! general-`a` affine group law, specifically because it has odd `y` where `G`'s is even, so +//! between them both compressed tags (`0x02`/`0x03`) are exercised. The encodings below were +//! derived from those coordinates independently in Python via plain big-endian byte packing. + +use bouncycastle_ec::bp512r1::Bp512r1FieldElement; +use bouncycastle_ec::bp512r1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::bp512r1_sec1::{decode, decode_point, encode_compressed, encode_uncompressed}; + +const TWO_G_X: [u64; 8] = [ + 0x080447def02f4850, 0xf698f404089a4cc5, 0x97116e702915a4f4, 0x957877f3a8f0f725, + 0xa30d3035f4cb6581, 0xd18d8141b8a18064, 0x0a63285758f399b3, 0x9f4945f680edf980, +]; +const TWO_G_Y: [u64; 8] = [ + 0xe269ad21be592e71, 0x63a42e4181fd929c, 0xa5c3b602b0960cbf, 0xfdf99bb1709c700f, + 0x5a2c366b03e5d1b2, 0x149ce1238d3f1e0f, 0x49826b716292f29d, 0x6d6b4b188b699c56, +]; + +const UNCOMPRESSED_G: [u8; 129] = [ + 0x04, 0x81, 0xae, 0xe4, 0xbd, 0xd8, 0x2e, 0xd9, 0x64, 0x5a, 0x21, 0x32, 0x2e, 0x9c, 0x4c, 0x6a, + 0x93, 0x85, 0xed, 0x9f, 0x70, 0xb5, 0xd9, 0x16, 0xc1, 0xb4, 0x3b, 0x62, 0xee, 0xf4, 0xd0, 0x09, + 0x8e, 0xff, 0x3b, 0x1f, 0x78, 0xe2, 0xd0, 0xd4, 0x8d, 0x50, 0xd1, 0x68, 0x7b, 0x93, 0xb9, 0x7d, + 0x5f, 0x7c, 0x6d, 0x50, 0x47, 0x40, 0x6a, 0x5e, 0x68, 0x8b, 0x35, 0x22, 0x09, 0xbc, 0xb9, 0xf8, + 0x22, 0x7d, 0xde, 0x38, 0x5d, 0x56, 0x63, 0x32, 0xec, 0xc0, 0xea, 0xbf, 0xa9, 0xcf, 0x78, 0x22, + 0xfd, 0xf2, 0x09, 0xf7, 0x00, 0x24, 0xa5, 0x7b, 0x1a, 0xa0, 0x00, 0xc5, 0x5b, 0x88, 0x1f, 0x81, + 0x11, 0xb2, 0xdc, 0xde, 0x49, 0x4a, 0x5f, 0x48, 0x5e, 0x5b, 0xca, 0x4b, 0xd8, 0x8a, 0x27, 0x63, + 0xae, 0xd1, 0xca, 0x2b, 0x2f, 0xa8, 0xf0, 0x54, 0x06, 0x78, 0xcd, 0x1e, 0x0f, 0x3a, 0xd8, 0x08, + 0x92, +]; + +const COMPRESSED_G: [u8; 65] = [ + 0x02, 0x81, 0xae, 0xe4, 0xbd, 0xd8, 0x2e, 0xd9, 0x64, 0x5a, 0x21, 0x32, 0x2e, 0x9c, 0x4c, 0x6a, + 0x93, 0x85, 0xed, 0x9f, 0x70, 0xb5, 0xd9, 0x16, 0xc1, 0xb4, 0x3b, 0x62, 0xee, 0xf4, 0xd0, 0x09, + 0x8e, 0xff, 0x3b, 0x1f, 0x78, 0xe2, 0xd0, 0xd4, 0x8d, 0x50, 0xd1, 0x68, 0x7b, 0x93, 0xb9, 0x7d, + 0x5f, 0x7c, 0x6d, 0x50, 0x47, 0x40, 0x6a, 0x5e, 0x68, 0x8b, 0x35, 0x22, 0x09, 0xbc, 0xb9, 0xf8, + 0x22, +]; + +const UNCOMPRESSED_TWO_G: [u8; 129] = [ + 0x04, 0x9f, 0x49, 0x45, 0xf6, 0x80, 0xed, 0xf9, 0x80, 0x0a, 0x63, 0x28, 0x57, 0x58, 0xf3, 0x99, + 0xb3, 0xd1, 0x8d, 0x81, 0x41, 0xb8, 0xa1, 0x80, 0x64, 0xa3, 0x0d, 0x30, 0x35, 0xf4, 0xcb, 0x65, + 0x81, 0x95, 0x78, 0x77, 0xf3, 0xa8, 0xf0, 0xf7, 0x25, 0x97, 0x11, 0x6e, 0x70, 0x29, 0x15, 0xa4, + 0xf4, 0xf6, 0x98, 0xf4, 0x04, 0x08, 0x9a, 0x4c, 0xc5, 0x08, 0x04, 0x47, 0xde, 0xf0, 0x2f, 0x48, + 0x50, 0x6d, 0x6b, 0x4b, 0x18, 0x8b, 0x69, 0x9c, 0x56, 0x49, 0x82, 0x6b, 0x71, 0x62, 0x92, 0xf2, + 0x9d, 0x14, 0x9c, 0xe1, 0x23, 0x8d, 0x3f, 0x1e, 0x0f, 0x5a, 0x2c, 0x36, 0x6b, 0x03, 0xe5, 0xd1, + 0xb2, 0xfd, 0xf9, 0x9b, 0xb1, 0x70, 0x9c, 0x70, 0x0f, 0xa5, 0xc3, 0xb6, 0x02, 0xb0, 0x96, 0x0c, + 0xbf, 0x63, 0xa4, 0x2e, 0x41, 0x81, 0xfd, 0x92, 0x9c, 0xe2, 0x69, 0xad, 0x21, 0xbe, 0x59, 0x2e, + 0x71, +]; + +const COMPRESSED_TWO_G: [u8; 65] = [ + 0x03, 0x9f, 0x49, 0x45, 0xf6, 0x80, 0xed, 0xf9, 0x80, 0x0a, 0x63, 0x28, 0x57, 0x58, 0xf3, 0x99, + 0xb3, 0xd1, 0x8d, 0x81, 0x41, 0xb8, 0xa1, 0x80, 0x64, 0xa3, 0x0d, 0x30, 0x35, 0xf4, 0xcb, 0x65, + 0x81, 0x95, 0x78, 0x77, 0xf3, 0xa8, 0xf0, 0xf7, 0x25, 0x97, 0x11, 0x6e, 0x70, 0x29, 0x15, 0xa4, + 0xf4, 0xf6, 0x98, 0xf4, 0x04, 0x08, 0x9a, 0x4c, 0xc5, 0x08, 0x04, 0x47, 0xde, 0xf0, 0x2f, 0x48, + 0x50, +]; + +fn fe(limbs: [u64; 8]) -> Bp512r1FieldElement { + Bp512r1FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_encode_uncompressed() { + assert_eq!(encode_uncompressed(&fe(G_X_LIMBS), &fe(G_Y_LIMBS)), UNCOMPRESSED_G); + assert_eq!(encode_uncompressed(&fe(TWO_G_X), &fe(TWO_G_Y)), UNCOMPRESSED_TWO_G); +} + +#[test] +fn known_answer_encode_compressed() { + assert_eq!( + encode_compressed(&fe(G_X_LIMBS), &fe(G_Y_LIMBS)), + COMPRESSED_G, + "G has even y -> tag 0x02" + ); + assert_eq!( + encode_compressed(&fe(TWO_G_X), &fe(TWO_G_Y)), + COMPRESSED_TWO_G, + "2G has odd y -> tag 0x03" + ); +} + +#[test] +fn known_answer_decode_uncompressed() { + let (x, y) = decode(&UNCOMPRESSED_G).expect("valid uncompressed point"); + assert_eq!(x, fe(G_X_LIMBS)); + assert_eq!(y, fe(G_Y_LIMBS)); +} + +#[test] +fn known_answer_decode_compressed() { + let (x, y) = decode(&COMPRESSED_G).expect("valid compressed point, tag 0x02"); + assert_eq!(x, fe(G_X_LIMBS)); + assert_eq!(y, fe(G_Y_LIMBS)); + + let (x2, y2) = decode(&COMPRESSED_TWO_G).expect("valid compressed point, tag 0x03"); + assert_eq!(x2, fe(TWO_G_X)); + assert_eq!(y2, fe(TWO_G_Y)); +} + +#[test] +fn round_trip_uncompressed_and_compressed() { + for (x, y) in [(G_X_LIMBS, G_Y_LIMBS), (TWO_G_X, TWO_G_Y)] { + let uncompressed = encode_uncompressed(&fe(x), &fe(y)); + assert_eq!(decode(&uncompressed), Some((fe(x), fe(y)))); + + let compressed = encode_compressed(&fe(x), &fe(y)); + assert_eq!(decode(&compressed), Some((fe(x), fe(y)))); + + assert!(decode_point(&uncompressed).unwrap().to_affine().is_some()); + } +} + +#[test] +fn decode_rejects_malformed_input() { + assert_eq!(decode(&[]), None); + assert_eq!(decode(&[0x04; 128]), None); + assert_eq!(decode(&[0x04; 130]), None); + assert_eq!(decode(&[0x02; 16]), None); + + let mut bad_tag_129 = UNCOMPRESSED_G; + bad_tag_129[0] = 0x02; + assert_eq!(decode(&bad_tag_129), None); + + let mut bad_tag_65 = COMPRESSED_G; + bad_tag_65[0] = 0x04; + assert_eq!(decode(&bad_tag_65), None); + + assert_eq!(decode(&[0x00]), None); + + let mut x_too_big = COMPRESSED_G; + x_too_big[1..65].fill(0xff); + assert_eq!(decode(&x_too_big), None); + + let mut no_root = COMPRESSED_G; + no_root[1..65].fill(0); + no_root[64] = 1; // x = 1, confirmed (in the Python derivation) to have no square root + assert_eq!(decode(&no_root), None); + + let mut not_on_curve = UNCOMPRESSED_G; + not_on_curve[128] ^= 0x01; + assert_eq!(decode(¬_on_curve), None); + + let mut y_too_big = UNCOMPRESSED_G; + y_too_big[65..129].fill(0xff); + assert_eq!(decode(&y_too_big), None); +} diff --git a/crypto/ec/tests/bp512r1_wnaf_tests.rs b/crypto/ec/tests/bp512r1_wnaf_tests.rs new file mode 100644 index 000000000..147537a59 --- /dev/null +++ b/crypto/ec/tests/bp512r1_wnaf_tests.rs @@ -0,0 +1,183 @@ +//! Known-answer tests for [`shamir_multiply`] (`[u]G + [v]Q`), brainpoolP512r1. +//! +//! `Q` below is `d*G` for a fixed test-only `d` (not a real key). Expected results were computed +//! independently in Python from the affine group law (general `a`), the same way as +//! `bp512r1_comb_tests.rs`, with `random.seed(808512)`. + +use bouncycastle_ec::bp512r1::Bp512r1FieldElement; +use bouncycastle_ec::bp512r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp512r1_point::Bp512r1JacobianPoint; +use bouncycastle_ec::bp512r1_scalar::{Bp512r1PublicScalar, Bp512r1Scalar}; +use bouncycastle_ec::bp512r1_wnaf::shamir_multiply; + +const Q_X: [u64; 8] = [ + 0x5816d744a9cf4eda, 0x14361f88a4d1fbcf, 0x6062798b3d4dc7f8, 0xdb6eba12aa0449c2, + 0x473c6bd692fef5a3, 0xc04b43d2d6f73a09, 0x07f88a2de934d573, 0x4d12cf5cdde21bb2, +]; +const Q_Y: [u64; 8] = [ + 0xb1a835f29de36a99, 0xd12a57b0461e9dcc, 0x08b9e3b064052a38, 0xe1d97bcd87b37b21, + 0xa6a5f2248c4ce531, 0x535577dcabf6f708, 0xeda6205f1acb7d00, 0x14ed25250ee66acc, +]; + +fn q() -> Bp512r1JacobianPoint { + Bp512r1JacobianPoint::from_affine( + Bp512r1FieldElement::from_limbs(Q_X), + Bp512r1FieldElement::from_limbs(Q_Y), + ) +} + +fn scalar(limbs: [u64; 8]) -> Bp512r1PublicScalar { + Bp512r1PublicScalar::from_limbs(limbs) +} + +fn assert_result_is(u: [u64; 8], v: [u64; 8], expected_x: [u64; 8], expected_y: [u64; 8]) { + let result = shamir_multiply(&scalar(u), &scalar(v), &q()); + let (rx, ry) = result.to_affine().expect("expected a non-infinity result"); + assert_eq!(rx, Bp512r1FieldElement::from_limbs(expected_x), "x mismatch for u={u:x?} v={v:x?}"); + assert_eq!(ry, Bp512r1FieldElement::from_limbs(expected_y), "y mismatch for u={u:x?} v={v:x?}"); +} + +#[test] +fn known_answer_large_u_v() { + let u: [u64; 8] = [ + 0xf72c1b3dd112ad3a, 0xd9fd486b4f8f85e2, 0x34d5bb94dd8909e5, 0xc59c44fd8eccd1f7, + 0x1391126a445a5cd9, 0x88dbc08da2a4e439, 0xfb5cfa3f94b30172, 0x8d4dc198c6f7e773, + ]; + let v: [u64; 8] = [ + 0x7f06791e62c1b6d2, 0x27c9a8e864d74086, 0x7f96016b69474a61, 0xefb048bd284ce7ec, + 0x561ceeee26f27710, 0x1e626f9a88af3fef, 0xefbbc00d77c8f0ba, 0x7daae794912ace93, + ]; + assert_result_is( + u, + v, + [ + 0xc65bbeb05d893442, 0x64ff7f51deda0175, 0x4c432c13e922cfdc, 0x84cba279920466bd, + 0x5152ab520b6ce4e1, 0xfcadb8d446c66aa0, 0xdfb41a87090f575d, 0x671fbd605746e874, + ], + [ + 0xc072dfbfa132b695, 0xc517df6b74f85468, 0x7f81693763db58f3, 0x6e153d88f4bfb6cd, + 0x0ced4d6f7d1e80e1, 0xf82d8b1771d492ed, 0xa46ad57f1c9f9116, 0x47b8dd07bb0163cd, + ], + ); +} + +#[test] +fn edge_case_v_is_zero_reduces_to_u_times_g() { + assert_result_is( + [5, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0], + [ + 0x500d6f7d9d9aaa5c, 0x17bc4f43d413540b, 0xc393c273727cf25d, 0xaadc73e8d9472bb0, + 0xae0ff1c9461693d2, 0x0a4abf8dd044a3c1, 0xe3c9bc8c2bf17781, 0x8672838ed83a55b9, + ], + [ + 0x62f8a1c2b51f7f35, 0xdb58bb174dcc0c77, 0xfa11d3cc5b06c0df, 0xa5a88c91f28d09eb, + 0xe1b69903a8863d2f, 0xea9d3a0a7a668f1e, 0x52b7a5643c936c09, 0x151d93c1de2ed9ee, + ], + ); +} + +#[test] +fn edge_case_u_is_zero_reduces_to_v_times_q() { + assert_result_is( + [0, 0, 0, 0, 0, 0, 0, 0], + [5, 0, 0, 0, 0, 0, 0, 0], + [ + 0x512bcfa72f89e064, 0x9d1f8fc6c5c0273c, 0xbbd9f199434f86c4, 0x5a083de4902c9681, + 0x9322c9c7aad24529, 0x78be7c030cee42eb, 0xc19eb0bdae3e7584, 0x70e532a86012cb25, + ], + [ + 0xd3cd888682e35709, 0x91d5589b896e9f10, 0xa7f4b26c141e68df, 0x3ef6461b33378824, + 0x29d1c225c0b68664, 0xc55d9ca3a6d1bdd9, 0x12e6f1ad0d1c63f4, 0x13ae6e16c77f51b4, + ], + ); +} + +#[test] +fn edge_case_both_zero_is_infinity() { + let result = shamir_multiply(&scalar([0; 8]), &scalar([0; 8]), &q()); + assert!(result.to_affine().is_none()); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 8] { + [ + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + ] + } +} + +#[test] +fn cross_checked_against_the_comb_multiplier_over_many_pseudorandom_scalars() { + // shamir_multiply(k, 0, anything) must equal comb_multiply_base_point(k): two independently + // implemented multipliers (fixed-base comb vs. interleaved wNAF) computing the same [k]G. + let mut rng = Xorshift64(0xE7C558A6A6E7F1DE); + for _ in 0..500 { + let k_limbs = rng.next_limbs(); + let via_comb = comb_multiply_base_point(&Bp512r1Scalar::from_limbs(k_limbs)); + let via_wnaf = + shamir_multiply(&Bp512r1PublicScalar::from_limbs(k_limbs), &scalar([0; 8]), &q()); + assert_eq!(via_comb.to_affine(), via_wnaf.to_affine(), "k = {k_limbs:x?}"); + } +} + +/// Every other test in this file uses one fixed `Q = dG`. The three `Q` values here each take a +/// path that one cannot: `Q = infinity` makes every `[v]Q` digit an addition of the identity, +/// `Q = G` makes the two precomputed odd-multiple tables identical (so the interleaved +/// additions hit the same-point case of `add_vartime`), and `Q = -G` makes them negatives of +/// each other (the opposite-point case, and with `u == v` a final result of infinity). Expected +/// values come from the fixed-base comb multiplier on the combined scalar, which is pinned by +/// its own known-answer tests. +#[test] +fn edge_case_q_is_infinity_g_or_minus_g() { + let g = Bp512r1JacobianPoint::from_affine( + Bp512r1FieldElement::from_limbs(bouncycastle_ec::bp512r1_domain::G_X_LIMBS), + Bp512r1FieldElement::from_limbs(bouncycastle_ec::bp512r1_domain::G_Y_LIMBS), + ); + let mut u = [0u64; 8]; + u[0] = 12345; + let mut v = [0u64; 8]; + v[0] = 6789; + let mut u_plus_v = [0u64; 8]; + u_plus_v[0] = 19134; + let mut u_minus_v = [0u64; 8]; + u_minus_v[0] = 5556; + let via_comb = + |k: [u64; 8]| comb_multiply_base_point(&Bp512r1Scalar::from_limbs(k)).to_affine(); + + // [u]G + [v]infinity == [u]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &Bp512r1JacobianPoint::INFINITY).to_affine(), + via_comb(u) + ); + // [u]G + [v]G == [u+v]G + assert_eq!(shamir_multiply(&scalar(u), &scalar(v), &g).to_affine(), via_comb(u_plus_v)); + // [u]G + [v](-G) == [u-v]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &g.negate()).to_affine(), + via_comb(u_minus_v) + ); + // [u]G + [u](-G) == infinity + assert!(shamir_multiply(&scalar(u), &scalar(u), &g.negate()).is_infinity().to_bool()); + // [0]G + [v]G == [v]G: the G table is never consulted, only Q's + assert_eq!(shamir_multiply(&scalar([0u64; 8]), &scalar(v), &g).to_affine(), via_comb(v)); +} diff --git a/crypto/ec/tests/montgomery_tests.rs b/crypto/ec/tests/montgomery_tests.rs new file mode 100644 index 000000000..a5b68bddf --- /dev/null +++ b/crypto/ec/tests/montgomery_tests.rs @@ -0,0 +1,432 @@ +//! Cross-validation for [`bouncycastle_ec::montgomery`]'s generic `widening_mul`/`redc` against +//! [`P256ScalarField`], whose own hand-written Montgomery arithmetic is already independently +//! verified (KATs, RFC 6979 vectors, wycheproof, and its own mutation-testing pass). `N_PRIME` and +//! `R_SQUARED_LIMBS` below are derived independently (`-n⁻¹ mod 2^64`, `R² mod n` computed directly +//! in Python from `N_LIMBS`, not copied from `p256_scalar`'s own private constants of the same +//! name) -- this is exactly the same shape of check a brand new curve gets, just against a curve +//! whose correct answer is already known, so it validates the generic module itself rather than +//! any particular brainpool curve's own parameters. + +use bouncycastle_ec::montgomery::{redc, widening_mul, widening_square}; +use bouncycastle_ec::nat; +use bouncycastle_ec::p256_scalar::{N_LIMBS, P256ScalarField}; + +const N_PRIME: u64 = 0xccd1c8aaee00bc4f; +const R_SQUARED_LIMBS: [u64; 4] = + [0x83244c95be79eea2, 0x4699799c49bd6fa6, 0x2845b2392b6bec59, 0x66e12d94f3d95620]; +/// `R mod n` (`R = 2^256`), independently computed the same way as `N_PRIME`/`R_SQUARED_LIMBS` +/// above -- used by [`reduce_once`]'s `extra == 1` case exactly as every hand-written per-curve +/// `redc` in this crate uses its own identically-derived constant (e.g. +/// [`bouncycastle_ec::p256_scalar`]'s private `R_LIMBS`). +const R_MOD_N_LIMBS: [u64; 4] = + [0x0c46353d039cdaaf, 0x4319055258e8617b, 0x0000000000000000, 0x00000000ffffffff]; + +/// `redc`'s output (`high + extra*R`, proven `< 2n`) reduced to the canonical `< n` value: if +/// `extra == 1`, `high + R_MOD_N_LIMBS` (no further reduction needed, mirroring every per-curve +/// `redc`'s identical final step); otherwise `high`, minus `n` once if `high >= n`. +fn reduce_once((high, extra): ([u64; 4], u64)) -> [u64; 4] { + if extra == 1 { + let (sum, _) = nat::add(&high, &R_MOD_N_LIMBS); + sum + } else { + let (diff, borrow) = nat::sub(&high, &N_LIMBS); + if borrow == 1 { high } else { diff } + } +} + +/// `a * b mod n` via the generic module: `widening_mul`, `redc` (bringing into Montgomery form via +/// `R_SQUARED_LIMBS` first, exactly as [`P256ScalarField::from_limbs`] does), then the same +/// single-conditional-subtraction final step every curve's own `redc` caller performs. +fn generic_mul_via_montgomery_form(a: [u64; 4], b: [u64; 4]) -> [u64; 4] { + fn to_montgomery(x: [u64; 4]) -> [u64; 4] { + let t = widening_mul::<4, 8>(&x, &R_SQUARED_LIMBS); + reduce_once(redc::<4, 8, 9>(&t, &N_LIMBS, N_PRIME)) + } + + let am = to_montgomery(a); + let bm = to_montgomery(b); + let t = widening_mul::<4, 8>(&am, &bm); + let product_montgomery = reduce_once(redc::<4, 8, 9>(&t, &N_LIMBS, N_PRIME)); + // Converting back out of Montgomery form: REDC of `product_montgomery` placed in the *low* + // half of a 2L-limb value with a zero high half (i.e. `T = product_montgomery`, `T < R`) + // computes `T * R^-1 mod n = product_montgomery * R^-1 mod n`, undoing the Montgomery scaling. + let mut wide = [0u64; 8]; + wide[..4].copy_from_slice(&product_montgomery); + reduce_once(redc::<4, 8, 9>(&wide, &N_LIMBS, N_PRIME)) +} + +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn generic_montgomery_matches_p256_scalar_field_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0xA11CE5A11CE5A11C); + for _ in 0..2000 { + let a = rng.next_limbs(); + let b = rng.next_limbs(); + + let expected = P256ScalarField::from_limbs(a).mul(&P256ScalarField::from_limbs(b)); + let got_limbs = generic_mul_via_montgomery_form(a, b); + let got = P256ScalarField::from_limbs(got_limbs); + assert_eq!(got, expected, "a={a:x?} b={b:x?}"); + } +} + +#[test] +fn generic_montgomery_matches_p256_scalar_field_worst_case() { + let n_minus_1 = { + let mut l = N_LIMBS; + l[0] -= 1; + l + }; + let expected = + P256ScalarField::from_limbs(n_minus_1).mul(&P256ScalarField::from_limbs(n_minus_1)); + let got = P256ScalarField::from_limbs(generic_mul_via_montgomery_form(n_minus_1, n_minus_1)); + assert_eq!(got, expected); +} + +/// [`generic_mul_via_montgomery_form`] for any width, with the modulus and its Montgomery +/// constants passed in -- the shape the brainpool curves actually use the module in. `redc`'s +/// `(high, extra)` result is reduced exactly as [`reduce_once`] does for P-256. +fn generic_mul_via_montgomery_form_at( + a: [u64; L], + b: [u64; L], + n: &[u64; L], + n_prime: u64, + r_mod_n: &[u64; L], + r_squared: &[u64; L], +) -> [u64; L] { + let reduce = |(high, extra): ([u64; L], u64)| -> [u64; L] { + if extra == 1 { + nat::add(&high, r_mod_n).0 + } else { + let (diff, borrow) = nat::sub(&high, n); + if borrow == 1 { high } else { diff } + } + }; + let to_montgomery = |x: [u64; L]| -> [u64; L] { + reduce(redc::(&widening_mul::(&x, r_squared), n, n_prime)) + }; + + let product_montgomery = reduce(redc::( + &widening_mul::(&to_montgomery(a), &to_montgomery(b)), + n, + n_prime, + )); + let mut wide = [0u64; L2]; + wide[..L].copy_from_slice(&product_montgomery); + reduce(redc::(&wide, n, n_prime)) +} + +/// `n`, `-n^-1 mod 2^64`, `R mod n` and `R^2 mod n` for bp384r1's order (`R = 2^384`), each +/// computed directly in Python from the RFC 5639 value of `n`, not copied from the crate. +mod bp384r1_consts { + pub const N_LIMBS: [u64; 6] = [ + 0x3b883202e9046565, 0xcf3ab6af6b7fc310, 0x1f166e6cac0425a7, 0x152f7109ed5456b3, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ]; + pub const N_PRIME: u64 = 0x5cfedd2a5cb5bb93; + pub const R_MOD_N_LIMBS: [u64; 6] = [ + 0xc477cdfd16fb9a9b, 0x30c5495094803cef, 0xe0e9919353fbda58, 0xead08ef612aba94c, + 0xf0a29081af19be20, 0x7346e17d5cc792d7, + ]; + pub const R_SQUARED_LIMBS: [u64; 6] = [ + 0xac4ed3a2de771c8e, 0x37264e202f2b6b6e, 0x2a927e3b9802688a, 0x574a74cb52d748ff, + 0x8f886dc965165fdb, 0x0ce8941a614e97c2, + ]; +} + +/// The generic module at `L = 6`, the width bp384r1 instantiates it at: `(n-1)^2 == 1` and +/// `(n-1)(n-2) == 2` (the largest products two canonical values can form, which is where the +/// `2L+1`-limb accumulator and its tail carry loop matter), plus one pseudorandom pair +/// (`random.seed(6289)`) whose expected value is Python's `(a * b) % n`. +#[test] +fn generic_montgomery_known_answers_at_l_6() { + use bp384r1_consts::*; + let n_minus_1 = [ + 0x3b883202e9046564, 0xcf3ab6af6b7fc310, 0x1f166e6cac0425a7, 0x152f7109ed5456b3, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ]; + let n_minus_2 = [ + 0x3b883202e9046563, 0xcf3ab6af6b7fc310, 0x1f166e6cac0425a7, 0x152f7109ed5456b3, + 0x0f5d6f7e50e641df, 0x8cb91e82a3386d28, + ]; + let mul = |a, b| { + generic_mul_via_montgomery_form_at::<6, 12, 13>( + a, b, &N_LIMBS, N_PRIME, &R_MOD_N_LIMBS, &R_SQUARED_LIMBS, + ) + }; + assert_eq!( + mul(n_minus_1, n_minus_1), + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000 + ], + "(n-1)^2 == 1" + ); + assert_eq!( + mul(n_minus_1, n_minus_2), + [ + 0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000 + ], + "(n-1)(n-2) == 2" + ); + assert_eq!( + mul( + n_minus_1, + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000 + ] + ), + n_minus_1, + "(n-1) * 1 == n-1" + ); + assert_eq!( + mul( + [ + 0x5f2ea6fdc5693ff0, 0x6ccf219bfc612a44, 0x00743629ae17c7b5, 0x69a1357a93b8b552, + 0x59f8cf15db16e623, 0x0694510ba9d9460b + ], + [ + 0xa038bbdbedc6f313, 0xdacf95e9e2e5ebc0, 0xf0261bde3402723a, 0xad661e728082ef22, + 0xa97e814518d0015c, 0x6f81b8ea97e2b0c0 + ] + ), + [ + 0xd72f0ce0ef2a841a, 0xbd07255c147f7805, 0x27f108c5cb9924ed, 0xda8440445f80d6b6, + 0x790c8ab40ef67fc3, 0x28b2b4a163ad74f8 + ] + ); + assert_eq!( + mul( + [ + 0xa038bbdbedc6f313, 0xdacf95e9e2e5ebc0, 0xf0261bde3402723a, 0xad661e728082ef22, + 0xa97e814518d0015c, 0x6f81b8ea97e2b0c0 + ], + [ + 0x5f2ea6fdc5693ff0, 0x6ccf219bfc612a44, 0x00743629ae17c7b5, 0x69a1357a93b8b552, + 0x59f8cf15db16e623, 0x0694510ba9d9460b + ] + ), + [ + 0xd72f0ce0ef2a841a, 0xbd07255c147f7805, 0x27f108c5cb9924ed, 0xda8440445f80d6b6, + 0x790c8ab40ef67fc3, 0x28b2b4a163ad74f8 + ], + "commuted" + ); +} + +/// `n`, `-n^-1 mod 2^64`, `R mod n` and `R^2 mod n` for bp512r1's order (`R = 2^512`), each +/// computed directly in Python from the RFC 5639 value of `n`, not copied from the crate. +mod bp512r1_consts { + pub const N_LIMBS: [u64; 8] = [ + 0xb58796829ca90069, 0x1db1d381085ddadd, 0x418661197fac1047, 0x553e5c414ca92619, + 0xd6639cca70330870, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ]; + pub const N_PRIME: u64 = 0xad49541f0f1b7027; + pub const R_MOD_N_LIMBS: [u64; 8] = [ + 0x4a78697d6356ff97, 0xe24e2c7ef7a22522, 0xbe799ee68053efb8, 0xaac1a3beb356d9e6, + 0x299c63358fccf78f, 0x34cf724c4c362df1, 0xc02b1951cc3603f8, 0x5522624724163b74, + ]; + pub const R_SQUARED_LIMBS: [u64; 8] = [ + 0xd2a3681ecda81671, 0x0886b75895283ddd, 0x3ec64bd033b7627f, 0xa6f230c72f0207e8, + 0xd7f9cc263b790de3, 0x723c37a22f16bbdf, 0x95df1b4c194b2e56, 0xa794586a718407b0, + ]; +} + +/// The generic module at `L = 8`, the width bp512r1 instantiates it at: `(n-1)^2 == 1` and +/// `(n-1)(n-2) == 2` (the largest products two canonical values can form, which is where the +/// `2L+1`-limb accumulator and its tail carry loop matter), plus one pseudorandom pair +/// (`random.seed(6289)`) whose expected value is Python's `(a * b) % n`. +#[test] +fn generic_montgomery_known_answers_at_l_8() { + use bp512r1_consts::*; + let n_minus_1 = [ + 0xb58796829ca90068, 0x1db1d381085ddadd, 0x418661197fac1047, 0x553e5c414ca92619, + 0xd6639cca70330870, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ]; + let n_minus_2 = [ + 0xb58796829ca90067, 0x1db1d381085ddadd, 0x418661197fac1047, 0x553e5c414ca92619, + 0xd6639cca70330870, 0xcb308db3b3c9d20e, 0x3fd4e6ae33c9fc07, 0xaadd9db8dbe9c48b, + ]; + let mul = |a, b| { + generic_mul_via_montgomery_form_at::<8, 16, 17>( + a, b, &N_LIMBS, N_PRIME, &R_MOD_N_LIMBS, &R_SQUARED_LIMBS, + ) + }; + assert_eq!( + mul(n_minus_1, n_minus_1), + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000 + ], + "(n-1)^2 == 1" + ); + assert_eq!( + mul(n_minus_1, n_minus_2), + [ + 0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000 + ], + "(n-1)(n-2) == 2" + ); + assert_eq!( + mul( + n_minus_1, + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000 + ] + ), + n_minus_1, + "(n-1) * 1 == n-1" + ); + assert_eq!( + mul( + [ + 0x51474e476d3e8250, 0xc40f92fed2b6845d, 0x36304de017a709cd, 0x5ee1276b928987ca, + 0xf87746982a1c7de0, 0x6f0bf816e68a9e11, 0x22f9463eead00edb, 0x78e16f8183e1482c + ], + [ + 0x71867f3260f10097, 0x3a070e67812e685d, 0xa66ec1f9c1388945, 0x4f6b9000c9f8b46f, + 0x1d964c0202ec66d3, 0xf53111408ef6534d, 0xc2702c675475b76d, 0x37b644bd7ac0cd7e + ] + ), + [ + 0x64cddde3d7e1ef15, 0x4e2ee5ed781ce49d, 0x4ec4b684d3baa7fc, 0x181b22cae55ce08f, + 0xb028dbb55501b1ee, 0x967989f61a2f39ee, 0x62b5711cdb6e1336, 0x5cadf0fb90459db1 + ] + ); + assert_eq!( + mul( + [ + 0x71867f3260f10097, 0x3a070e67812e685d, 0xa66ec1f9c1388945, 0x4f6b9000c9f8b46f, + 0x1d964c0202ec66d3, 0xf53111408ef6534d, 0xc2702c675475b76d, 0x37b644bd7ac0cd7e + ], + [ + 0x51474e476d3e8250, 0xc40f92fed2b6845d, 0x36304de017a709cd, 0x5ee1276b928987ca, + 0xf87746982a1c7de0, 0x6f0bf816e68a9e11, 0x22f9463eead00edb, 0x78e16f8183e1482c + ] + ), + [ + 0x64cddde3d7e1ef15, 0x4e2ee5ed781ce49d, 0x4ec4b684d3baa7fc, 0x181b22cae55ce08f, + 0xb028dbb55501b1ee, 0x967989f61a2f39ee, 0x62b5711cdb6e1336, 0x5cadf0fb90459db1 + ], + "commuted" + ); +} + +/// `widening_square` is the one routine in the generic module nothing above reaches (the +/// brainpool `square`s call it, but only their own `square_agrees_with_mul` tests see it, and +/// only through a reduction). Pinned against `widening_mul` at every width the crate +/// instantiates, plus the all-ones known answer, whose doubling pass carries out of every limb. + +#[test] +fn widening_square_agrees_with_widening_mul_and_the_all_ones_known_answer_at_l_4() { + // (2^256 - 1)^2 = 2^512 - 2^257 + 1, computed in Python: limb 0 is 1, limb 4 + // is 2^64 - 2, and limbs 5..=7 are all ones. + let all_ones = [u64::MAX; 4]; + let expected: [u64; 8] = [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0xfffffffffffffffe, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + ]; + assert_eq!(widening_square::<4, 8>(&all_ones), expected); + assert_eq!(widening_mul::<4, 8>(&all_ones, &all_ones), expected); + + let mut rng = Xorshift64(0x5EED_0000_0000_0004); + for _ in 0..2000 { + let mut a = [0u64; 4]; + for limb in a.iter_mut() { + *limb = rng.next_u64(); + } + assert_eq!(widening_square::<4, 8>(&a), widening_mul::<4, 8>(&a, &a), "a = {a:x?}"); + } +} + +#[test] +fn widening_square_agrees_with_widening_mul_and_the_all_ones_known_answer_at_l_6() { + // (2^384 - 1)^2 = 2^768 - 2^385 + 1, computed in Python: limb 0 is 1, limb 6 + // is 2^64 - 2, and limbs 7..=11 are all ones. + let all_ones = [u64::MAX; 6]; + let expected: [u64; 12] = [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0xfffffffffffffffe, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + ]; + assert_eq!(widening_square::<6, 12>(&all_ones), expected); + assert_eq!(widening_mul::<6, 12>(&all_ones, &all_ones), expected); + + let mut rng = Xorshift64(0x5EED_0000_0000_0006); + for _ in 0..2000 { + let mut a = [0u64; 6]; + for limb in a.iter_mut() { + *limb = rng.next_u64(); + } + assert_eq!(widening_square::<6, 12>(&a), widening_mul::<6, 12>(&a, &a), "a = {a:x?}"); + } +} + +#[test] +fn widening_square_agrees_with_widening_mul_and_the_all_ones_known_answer_at_l_8() { + // (2^512 - 1)^2 = 2^1024 - 2^513 + 1, computed in Python: limb 0 is 1, limb 8 + // is 2^64 - 2, and limbs 9..=15 are all ones. + let all_ones = [u64::MAX; 8]; + let expected: [u64; 16] = [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0xfffffffffffffffe, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + ]; + assert_eq!(widening_square::<8, 16>(&all_ones), expected); + assert_eq!(widening_mul::<8, 16>(&all_ones, &all_ones), expected); + + let mut rng = Xorshift64(0x5EED_0000_0000_0008); + for _ in 0..2000 { + let mut a = [0u64; 8]; + for limb in a.iter_mut() { + *limb = rng.next_u64(); + } + assert_eq!(widening_square::<8, 16>(&a), widening_mul::<8, 16>(&a, &a), "a = {a:x?}"); + } +} + +#[test] +fn widening_square_agrees_with_widening_mul_and_the_all_ones_known_answer_at_l_9() { + // (2^576 - 1)^2 = 2^1152 - 2^577 + 1, computed in Python: limb 0 is 1, limb 9 + // is 2^64 - 2, and limbs 10..=17 are all ones. + let all_ones = [u64::MAX; 9]; + let expected: [u64; 18] = [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0xfffffffffffffffe, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, + ]; + assert_eq!(widening_square::<9, 18>(&all_ones), expected); + assert_eq!(widening_mul::<9, 18>(&all_ones, &all_ones), expected); + + let mut rng = Xorshift64(0x5EED_0000_0000_0009); + for _ in 0..2000 { + let mut a = [0u64; 9]; + for limb in a.iter_mut() { + *limb = rng.next_u64(); + } + assert_eq!(widening_square::<9, 18>(&a), widening_mul::<9, 18>(&a, &a), "a = {a:x?}"); + } +} diff --git a/crypto/ec/tests/nat_tests.rs b/crypto/ec/tests/nat_tests.rs new file mode 100644 index 000000000..53785a19a --- /dev/null +++ b/crypto/ec/tests/nat_tests.rs @@ -0,0 +1,77 @@ +use bouncycastle_ec::nat; + +#[test] +fn add_no_carry() { + let a: [u64; 4] = [1, 2, 3, 4]; + let b: [u64; 4] = [10, 20, 30, 40]; + let (sum, carry) = nat::add(&a, &b); + assert_eq!(sum, [11, 22, 33, 44]); + assert_eq!(carry, 0); +} + +#[test] +fn add_with_ripple_carry() { + let a: [u64; 4] = [u64::MAX, u64::MAX, u64::MAX, 0]; + let b: [u64; 4] = [1, 0, 0, 0]; + let (sum, carry) = nat::add(&a, &b); + // 0xff..f, 0xff..f, 0xff..f, 0 + 1 -> carries all the way through the first three limbs + assert_eq!(sum, [0, 0, 0, 1]); + assert_eq!(carry, 0); +} + +#[test] +fn add_with_carry_out() { + let a: [u64; 4] = [u64::MAX; 4]; + let b: [u64; 4] = [1, 0, 0, 0]; + let (sum, carry) = nat::add(&a, &b); + assert_eq!(sum, [0, 0, 0, 0]); + assert_eq!(carry, 1); +} + +#[test] +fn sub_no_borrow() { + let a: [u64; 4] = [10, 20, 30, 40]; + let b: [u64; 4] = [1, 2, 3, 4]; + let (diff, borrow) = nat::sub(&a, &b); + assert_eq!(diff, [9, 18, 27, 36]); + assert_eq!(borrow, 0); +} + +#[test] +fn sub_with_ripple_borrow() { + let a: [u64; 4] = [0, 0, 0, 1]; + let b: [u64; 4] = [1, 0, 0, 0]; + let (diff, borrow) = nat::sub(&a, &b); + assert_eq!(diff, [u64::MAX, u64::MAX, u64::MAX, 0]); + assert_eq!(borrow, 0); +} + +#[test] +fn sub_with_borrow_out() { + let a: [u64; 4] = [0, 0, 0, 0]; + let b: [u64; 4] = [1, 0, 0, 0]; + let (diff, borrow) = nat::sub(&a, &b); + assert_eq!(diff, [u64::MAX; 4]); + assert_eq!(borrow, 1); +} + +#[test] +fn add_sub_are_inverses() { + let a: [u64; 4] = + [0x1122334455667788, 0x99aabbccddeeff00, 0x0102030405060708, 0x0f0e0d0c0b0a0908]; + let b: [u64; 4] = + [0xfedcba9876543210, 0x0123456789abcdef, 0xdeadbeefcafebabe, 0x1234567890abcdef]; + let (sum, carry) = nat::add(&a, &b); + let (back, borrow) = nat::sub(&sum, &b); + assert_eq!(back, a); + // whatever carry add produced, subtracting b back out produces the matching borrow + assert_eq!(borrow, carry); +} + +#[test] +fn is_zero() { + assert!(nat::is_zero(&[0u64; 4]).to_bool()); + assert!(!nat::is_zero(&[1, 0, 0, 0]).to_bool()); + assert!(!nat::is_zero(&[0, 0, 0, 1]).to_bool()); + assert!(!nat::is_zero(&[0, 0, 1, 0]).to_bool()); +} diff --git a/crypto/ec/tests/p256_comb_tests.rs b/crypto/ec/tests/p256_comb_tests.rs new file mode 100644 index 000000000..67079d25a --- /dev/null +++ b/crypto/ec/tests/p256_comb_tests.rs @@ -0,0 +1,96 @@ +//! Known-answer tests for [`comb_multiply_base_point`] (`[k]G`). +//! +//! Small-`k` vectors reuse the `k*G` values from `p256_point_tests.rs` (independently computed +//! from the SP 800-186 Appendix A.1.1 affine group law). The larger `k` vectors below were +//! generated the same way, with `random.seed(303)`, plus `k = n-1` (the edge case where `[k]G = +//! -G`, since `n*G` is the identity). + +use bouncycastle_ec::p256::P256FieldElement; +use bouncycastle_ec::p256_comb::comb_multiply_base_point; +use bouncycastle_ec::p256_scalar::P256Scalar; + +fn scalar(limbs: [u64; 4]) -> P256Scalar { + P256Scalar::from_limbs(limbs) +} + +fn assert_result_is(k_limbs: [u64; 4], x: [u64; 4], y: [u64; 4]) { + let result = comb_multiply_base_point(&scalar(k_limbs)); + let (rx, ry) = result.to_affine().expect("[k]G must not be infinity for k in (0, n)"); + assert_eq!(rx, P256FieldElement::from_limbs(x), "x mismatch for k = {k_limbs:x?}"); + assert_eq!(ry, P256FieldElement::from_limbs(y), "y mismatch for k = {k_limbs:x?}"); +} + +#[test] +fn known_answer_small_k() { + // k=1 (G), k=2, k=3, k=5, k=7, k=11, k=13 -- same values as p256_point_tests.rs's P1..P13. + assert_result_is( + [1, 0, 0, 0], + [0xf4a13945d898c296, 0x77037d812deb33a0, 0xf8bce6e563a440f2, 0x6b17d1f2e12c4247], + [0xcbb6406837bf51f5, 0x2bce33576b315ece, 0x8ee7eb4a7c0f9e16, 0x4fe342e2fe1a7f9b], + ); + assert_result_is( + [2, 0, 0, 0], + [0xa60b48fc47669978, 0xc08969e277f21b35, 0x8a52380304b51ac3, 0x7cf27b188d034f7e], + [0x9e04b79d227873d1, 0xba7dade63ce98229, 0x293d9ac69f7430db, 0x07775510db8ed040], + ); + assert_result_is( + [3, 0, 0, 0], + [0xfb41661bc6e7fd6c, 0xe6c6b721efada985, 0xc8f7ef951d4bf165, 0x5ecbe4d1a6330a44], + [0x9a79b127a27d5032, 0xd82ab036384fb83d, 0x374b06ce1a64a2ec, 0x8734640c4998ff7e], + ); + assert_result_is( + [7, 0, 0, 0], + [0x300628703187b2a3, 0x7ef9f8b8a80fef5b, 0x25bb30667c01fb60, 0x8e533b6fa0bf7b46], + [0xc55e1a86c1f400b4, 0x53c73633cb041b21, 0x6d069f83a6f59000, 0x73eb1dbde0331836], + ); + assert_result_is( + [13, 0, 0, 0], + [0x98e15d9d46072c01, 0x792e284b65ead58a, 0x61805df2d85ee2fc, 0x177c837ae0ac495a], + [0x9c43bbe2efc7bfd8, 0x26ee14c3a1fb4df3, 0xa24091adb40f4e72, 0x63bb58cd4ebea558], + ); +} + +#[test] +fn known_answer_large_k() { + assert_result_is( + [0x3d7c9ec7081ab44e, 0xbd8ec9a1f80385ed, 0x17857e0831e5554e, 0x90f26b82fe915329], + [0x79ce9c83502a5578, 0xc619b85acd90b588, 0x11dae3072ce0c11b, 0xce802709673a3576], + [0x41dd580f255b3b01, 0x7bddfdd6fa7a185a, 0x9bb6d2c693f682ff, 0x568f5771239c118b], + ); + assert_result_is( + [0x9f8573c9f25dc994, 0x9115361f42389a31, 0xfc83ab74842a0944, 0x285f078965f5a299], + [0xbe5c2efa594cb48e, 0x696aaf1b094a0c0b, 0xcdc19ac4148aff9c, 0x7738f3399f918ccf], + [0x9b7a003b13bfc146, 0xa47a04fb6f1addf8, 0x9d808272ab79b368, 0x877990fc238f16b0], + ); + assert_result_is( + [0x790d02ba68e2b293, 0x952cb98dca28e0ce, 0x1699fd1dd3e61f5f, 0x874ea8e941ac9159], + [0xd76531a06fcb9e02, 0xeaa2bf9bba167f7a, 0xe8e48ec2676f3496, 0x0eaaf59c3e417ccf], + [0x28cefc08078ab1e2, 0xcaa9f65e6dfaa4d6, 0x177bb38b4e337bc7, 0x7bf64c66ed9874b9], + ); + assert_result_is( + [0xc59a61378b3dda8e, 0x083d8f37af9e4e0d, 0xd253304398079d69, 0xb1e815b91f17e93f], + [0xe16fd84feccd5f54, 0x54dd53df8b2bc858, 0x74ca22b12e5aa2e3, 0x83dbb91599c15293], + [0xcca8553f261cb315, 0x9752d7c526d1777b, 0x356b80cb0ee111d8, 0xcae8485e5e74b3a6], + ); +} + +#[test] +fn edge_case_k_equals_n_minus_1() { + // (n-1)*G == -G: same x as G, negated y. + assert_result_is( + [0xf3b9cac2fc632550, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000], + [0xf4a13945d898c296, 0x77037d812deb33a0, 0xf8bce6e563a440f2, 0x6b17d1f2e12c4247], + [0x3449bf97c840ae0a, 0xd431cca994cea131, 0x711814b583f061e9, 0xb01cbd1c01e58065], + ); +} + +/// `k = 0` is the one scalar for which `[k]G` is the identity: every comb digit is `0`, so every +/// round selects table entry `0` (the infinity sentinel) and the accumulator never leaves +/// `INFINITY`. Nothing in the signing path can produce it (`k` is in `[1, n-1]` by construction), +/// which is exactly why it needs pinning here rather than being left to chance. +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let result = comb_multiply_base_point(&scalar([0u64; 4])); + assert!(result.is_infinity().to_bool()); + assert!(result.to_affine().is_none()); +} diff --git a/crypto/ec/tests/p256_extra_bits_tests.rs b/crypto/ec/tests/p256_extra_bits_tests.rs new file mode 100644 index 000000000..fbe5ec9bf --- /dev/null +++ b/crypto/ec/tests/p256_extra_bits_tests.rs @@ -0,0 +1,140 @@ +//! Known-answer tests for [`reduce_wide_bits_mod_n_minus_1`] (FIPS 186-5 Appendix A.4.1 steps +//! 3-5): `x = bits2int(X)`, `x = x mod (n-1)`, `x = x + 1`. Expected values computed independently +//! in Python via `(int.from_bytes(bytes, 'big') % (n - 1)) + 1` with `n` from SP 800-186 §3.2.1.3, +//! not derived from this crate's own reduction code -- existing round-trip tests elsewhere +//! (`keygen`/`sign_rng`) only ever check that the reduced value is self-consistent with +//! itself, which can't distinguish a correct reduction from a systematically wrong one (this is +//! exactly what a `cargo mutants` run on `reduce_wide_bits_mod_n_minus_1`'s bit-extraction and +//! carry-correction surfaced: several bit-level mutants survived because nothing here pinned the +//! function against an independent reference). + +use bouncycastle_ec::p256_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_hex::decode as hex_decode; + +fn bytes32(hex: &str) -> [u8; 32] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +#[test] +fn known_answer_repeating_pattern() { + let input: Vec = [0x01u8, 0x02, 0x03, 0x04].iter().cloned().cycle().take(44).collect(); + let expected = bytes32("01020303ff419cfdb5fae98dc534e5e6caac2bc95762898fec3558aa95c8f885"); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_ff() { + let input = [0xffu8; 44]; + let expected = bytes32("fffffffe431905549c0166cd652e96b789b1054851cc17b9e7739585f8c64aa0"); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_zero_yields_one() { + // bits2int(0) = 0; 0 mod (n-1) = 0; +1 = 1 -- the smallest value Appendix A.4.1 can produce. + let input = [0u8; 44]; + let mut expected = [0u8; 32]; + expected[31] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +/// The two boundaries of Appendix A.4.1's own arithmetic: `n - 1` is the modulus of step 4, so it +/// reduces to `0` and step 5 makes it `1`; `n - 2` is the largest residue, so it comes out as +/// `n - 1`, the top of the `[1, n-1]` output interval. Both inputs are left-padded to the DRBG +/// output width this module is fed with. +#[test] +fn known_answer_at_the_n_minus_1_boundary() { + let n = bouncycastle_ec::p256_sec1::be_bytes_from_limbs(&bouncycastle_ec::p256_scalar::N_LIMBS); + let mut n_minus_1 = n; + n_minus_1[31] -= 1; // n is odd, so neither of these subtractions borrows + let mut n_minus_2 = n; + n_minus_2[31] -= 2; + + let mut input = [0u8; 44]; + input[12..].copy_from_slice(&n_minus_1); + let mut one = [0u8; 32]; + one[31] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), one, "n - 1 -> 1"); + + input[12..].copy_from_slice(&n_minus_2); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), n_minus_1, "n - 2 -> n - 1"); +} + +/// The bit-serial algorithm this module implemented before switching to Barrett reduction, kept +/// here as an independent reference: Horner's rule over the input bits, doubling the running +/// remainder and subtracting `n - 1` when it is exceeded, with the carry out of the top limb folded +/// back in as `2^256 mod (n-1) = 2^256 - (n-1)`. Slow, obviously correct, and sharing no +/// code with the Barrett path beyond `nat`'s add and sub. +fn reference_bit_serial(bytes: &[u8]) -> [u8; 32] { + use bouncycastle_ec::nat; + let n = bouncycastle_ec::p256_scalar::N_LIMBS; + let mut m = n; + m[0] -= 1; // n is odd + let two_pow_mod_m = nat::sub(&[0u64; 4], &m).0; // 2^256 - m, wrapping + let mut acc = [0u64; 4]; + for &byte in bytes { + for bit_idx in (0..8).rev() { + let (mut doubled, carry) = nat::add(&acc, &acc); + doubled[0] |= ((byte >> bit_idx) & 1) as u64; + if carry == 1 { + doubled = nat::add(&doubled, &two_pow_mod_m).0; + } + let (reduced, borrow) = nat::sub(&doubled, &m); + acc = if borrow == 1 { doubled } else { reduced }; + } + } + let plus_one = nat::add(&acc, &[1u64, 0, 0, 0]).0; + bouncycastle_ec::p256_sec1::be_bytes_from_limbs(&plus_one) +} + +/// xorshift64*, fixed seed, as in the `ec` crate's property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } +} + +/// Barrett against the bit-serial reference over pseudorandom inputs at every width from one byte +/// up to the 63-byte maximum the function accepts, biased towards the widths actually drawn +/// (44 bytes) and towards values with long runs of set bits, where Barrett's quotient estimate is +/// furthest off and the most conditional subtractions are needed. +#[test] +fn agrees_with_the_bit_serial_reference_over_many_pseudorandom_inputs() { + let mut rng = Xorshift64(0xBA77E770_0000_0004); + for i in 0..3000 { + let len = match i % 4 { + 0 => 44, + 1 => 63, + _ => 1 + (rng.next_u64() as usize % 63), + }; + let mut bytes = vec![0u8; len]; + for chunk in bytes.chunks_mut(8) { + let word = rng.next_u64().to_be_bytes(); + chunk.copy_from_slice(&word[..chunk.len()]); + } + if i % 3 == 0 { + for b in bytes.iter_mut().take(len / 2) { + *b = 0xff; + } + } + assert_eq!( + reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), + reference_bit_serial(&bytes), + "len = {len}, bytes = {bytes:02x?}" + ); + } +} + +#[test] +fn maximum_width_all_ones_input() { + // 2^504 - 1, the largest input accepted: Barrett's precondition holds with room to spare. + let bytes = [0xffu8; 63]; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), reference_bit_serial(&bytes)); +} diff --git a/crypto/ec/tests/p256_field_tests.rs b/crypto/ec/tests/p256_field_tests.rs new file mode 100644 index 000000000..7cb703f30 --- /dev/null +++ b/crypto/ec/tests/p256_field_tests.rs @@ -0,0 +1,460 @@ +//! Known-answer tests for [`P256FieldElement`] arithmetic. +//! +//! `p = 2**256 - 2**224 + 2**192 + 2**96 - 1` has no dedicated NIST-published field-arithmetic KAT +//! suite (SP 800-186 gives only the domain parameters; `nist_ecc.txt`'s KATs are point multiples, +//! not raw field operations). The expected values below were computed independently in Python +//! (arbitrary-precision integers, not from recall), by: +//! +//! ```text +//! p = 2**256 - 2**224 + 2**192 + 2**96 - 1 +//! random.seed(42) +//! vals = [random.randrange(1, p) for _ in range(6)] + [0] +//! # for each pair (i, j) with i < j: (vals[i]+vals[j]) % p, (vals[i]-vals[j]) % p, (vals[i]*vals[j]) % p +//! # for each i: (-vals[i]) % p, pow(vals[i], p-2, p) if vals[i] != 0 else 0 +//! ``` + +use bouncycastle_ec::p256::P256FieldElement; + +const VALS_0: [u64; 4] = + [0x1c80317fa3b1799e, 0xbdd640fb06671ad1, 0x3eb13b9046685257, 0x23b8c1e9392456de]; +const VALS_1: [u64; 4] = + [0x1a3d1fa7bc8960aa, 0xbd9c66b3ad3c2d6d, 0x8b9d2434e465e150, 0x972a846916419f82]; +const VALS_2: [u64; 4] = + [0x0822e8f36c03119a, 0x17fc695a07a0ca6e, 0x3b8faa1837f8a88b, 0x9a1de644815ef6d1]; +const VALS_3: [u64; 4] = + [0x8fadc1a606cb0fb4, 0xb74d0fb132e70629, 0xb38a088ca65ed389, 0x6b65a6a48b8148f6]; +const VALS_4: [u64; 4] = + [0x72ff5d2a386ecbe1, 0x4737819096da1dac, 0xde8a774bcf36d58b, 0xc241330b01a9e71f]; +const VALS_5: [u64; 4] = + [0x28df6ec4ce4a2bbe, 0x6c307511b2b9437a, 0x47229389571aa876, 0x371ecd7b27cd8130]; +const VALS_6: [u64; 4] = + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; + +const ADD_0_1: [u64; 4] = + [0x36bd5127603ada48, 0x7b72a7aeb3a3483e, 0xca4e5fc52ace33a8, 0xbae346524f65f660]; +const SUB_0_1: [u64; 4] = + [0x024311d7e72818f3, 0x0039da48592aed64, 0xb314175b62027107, 0x8c8e3d7f22e2b75c]; +const MUL_0_1: [u64; 4] = + [0x1d498e4226541916, 0xa3a2af11401c3eaf, 0xb8b424088cb8c1fd, 0x126c3f0c9d0773ed]; +const ADD_0_2: [u64; 4] = + [0x24a31a730fb48b38, 0xd5d2aa550e07e53f, 0x7a40e5a87e60fae2, 0xbdd6a82dba834daf]; +const SUB_0_2: [u64; 4] = + [0x145d488c37ae6803, 0xa5d9d7a1fec65063, 0x032191780e6fa9cc, 0x899adba3b7c5600e]; +const MUL_0_2: [u64; 4] = + [0xdf30423481f7198f, 0x2df6ba6df741535f, 0xdc109c36de71409a, 0x092f3ed335b6eca9]; +const ADD_0_3: [u64; 4] = + [0xac2df325aa7c8952, 0x752350ac394e20fa, 0xf23b441cecc725e1, 0x8f1e688dc4a59fd4]; +const SUB_0_3: [u64; 4] = + [0x8cd26fd99ce669e9, 0x0689314ad38014a7, 0x8b273303a0097ece, 0xb8531b43ada30de8]; +const MUL_0_3: [u64; 4] = + [0x881a465ba587986c, 0x11a4b476c261c091, 0xb72ee7a8ac8176ac, 0xfbf5f90d5f0ce475]; +const ADD_0_4: [u64; 4] = + [0x8f7f8ea9dc20457f, 0x050dc28b9d41387d, 0x1d3bb2dc159f27e3, 0xe5f9f4f43ace3dfe]; +const SUB_0_4: [u64; 4] = + [0xa980d4556b42adbc, 0x769ebf6b6f8cfd24, 0x6026c44477317ccc, 0x61778edd377a6fbf]; +const MUL_0_4: [u64; 4] = + [0x610d4195b1cf6321, 0x5ad6afddd9b6d95b, 0xb2e3da7fc0dd0f06, 0xd8d65ef5e833bfff]; +const ADD_0_5: [u64; 4] = + [0x455fa04471fba55c, 0x2a06b60cb9205e4b, 0x85d3cf199d82face, 0x5ad78f6460f1d80e]; +const SUB_0_5: [u64; 4] = + [0xf3a0c2bad5674ddf, 0x51a5cbea53add756, 0xf78ea806ef4da9e1, 0xec99f46d1156d5ae]; +const MUL_0_5: [u64; 4] = + [0x4dd8c536b142c4fd, 0x052580f7ef851245, 0x31aac471aea9b11d, 0x21fa0b52a0c75353]; +const ADD_0_6: [u64; 4] = VALS_0; +const SUB_0_6: [u64; 4] = VALS_0; +const MUL_0_6: [u64; 4] = [0, 0, 0, 0]; +const ADD_1_2: [u64; 4] = + [0x2260089b288c7245, 0xd598d00cb4dcf7db, 0xc72cce4d1c5e89db, 0x31486aae97a09652]; +const SUB_1_2: [u64; 4] = + [0x121a36b450864f0f, 0xa59ffd5aa59b62ff, 0x500d7a1cac6d38c5, 0xfd0c9e2394e2a8b2]; +const MUL_1_2: [u64; 4] = + [0x8584d1073c9a7562, 0x2f835ac0a1032cee, 0xa4a1e78000501c71, 0xd18e1d03521d8a24]; +const ADD_1_3: [u64; 4] = + [0xa9eae14dc354705f, 0x74e97663e0233396, 0x3f272cc18ac4b4da, 0x02902b0ea1c2e878]; +const SUB_1_3: [u64; 4] = + [0x8a8f5e01b5be50f6, 0x064f57027a552743, 0xd8131ba83e070dc7, 0x2bc4ddc48ac0568b]; +const MUL_1_3: [u64; 4] = + [0x3c069230362b4f5e, 0x5c13926318333ff5, 0xff785333b1899926, 0x5c7b8e512999745f]; +const ADD_1_4: [u64; 4] = + [0x8d3c7cd1f4f82c8c, 0x04d3e84344164b19, 0x6a279b80b39cb6dc, 0x596bb77517eb86a1]; +const SUB_1_4: [u64; 4] = + [0xa73dc27d841a94c8, 0x7664e52416620fc0, 0xad12ace9152f0bc5, 0xd4e9515d1497b863]; +const MUL_1_4: [u64; 4] = + [0xf7b6bdb56011d920, 0xeac8a5b5fc56e9c7, 0xf35c056382048027, 0x621421739d4404a4]; +const ADD_1_5: [u64; 4] = + [0x431c8e6c8ad38c68, 0x29ccdbc55ff570e7, 0xd2bfb7be3b8089c7, 0xce4951e43e0f20b2]; +const SUB_1_5: [u64; 4] = + [0xf15db0e2ee3f34ec, 0x516bf1a1fa82e9f2, 0x447a90ab8d4b38da, 0x600bb6edee741e52]; +const MUL_1_5: [u64; 4] = + [0x7aefe2a7380cb984, 0x276651ef59a69959, 0xd5f63204a5a0805d, 0x3a460936b7b1fb61]; +const ADD_1_6: [u64; 4] = VALS_1; +const SUB_1_6: [u64; 4] = VALS_1; +const MUL_1_6: [u64; 4] = [0, 0, 0, 0]; +const ADD_2_3: [u64; 4] = + [0x97d0aa9972ce214f, 0xcf49790a3a87d097, 0xef19b2a4de577c14, 0x05838cea0ce03fc6]; +const SUB_2_3: [u64; 4] = + [0x7875274d653801e6, 0x60af59a8d4b9c444, 0x8805a18b9199d501, 0x2eb83f9ff5ddadda]; +const MUL_2_3: [u64; 4] = + [0xdead4cd976d54360, 0xe597ff5ff7898b58, 0x321ea3afaf4b9da9, 0x1c328f4a59c7d411]; +const ADD_2_4: [u64; 4] = + [0x7b22461da471dd7c, 0x5f33eae99e7ae81a, 0x1a1a2164072f7e16, 0x5c5f19508308ddf0]; +const SUB_2_4: [u64; 4] = + [0x95238bc9339445b8, 0xd0c4e7ca70c6acc1, 0x5d0532cc68c1d2ff, 0xd7dcb3387fb50fb2]; +const MUL_2_4: [u64; 4] = + [0x87d707c1e728f0ce, 0x859152342746f588, 0xd8921a986f162a54, 0xb3e69f5315d84b38]; +const ADD_2_5: [u64; 4] = + [0x310257b83a4d3d58, 0x842cde6bba5a0de8, 0x82b23da18f135101, 0xd13cb3bfa92c7801]; +const SUB_2_5: [u64; 4] = + [0xdf437a2e9db8e5dc, 0xabcbf44854e786f3, 0xf46d168ee0de0014, 0x62ff18c9599175a0]; +const MUL_2_5: [u64; 4] = + [0xa2b4c338c1505d38, 0x27a89e17c7e8bc51, 0x871497c2958968b8, 0x65fe83f8c19119df]; +const ADD_2_6: [u64; 4] = VALS_2; +const SUB_2_6: [u64; 4] = VALS_2; +const MUL_2_6: [u64; 4] = [0, 0, 0, 0]; +const ADD_3_4: [u64; 4] = + [0x02ad1ed03f39db96, 0xfe849140c9c123d6, 0x92147fd87595a914, 0x2da6d9b08d2b3015]; +const SUB_3_4: [u64; 4] = + [0x1cae647bce5c43d2, 0x70158e219c0ce87d, 0xd4ff9140d727fdfe, 0xa924739889d761d7]; +const MUL_3_4: [u64; 4] = + [0x3551ad12eda55b13, 0xc13f202fb88dd5b2, 0xcd395f9e7f76d48f, 0xf9843ab669fa865c]; +const ADD_3_5: [u64; 4] = + [0xb88d306ad5153b72, 0x237d84c2e5a049a3, 0xfaac9c15fd797c00, 0xa284741fb34eca26]; +const SUB_3_5: [u64; 4] = + [0x66ce52e13880e3f6, 0x4b1c9a9f802dc2af, 0x6c6775034f442b13, 0x3446d92963b3c7c6]; +const MUL_3_5: [u64; 4] = + [0x5af0d58f0d336fc8, 0x321953464e5b34c2, 0x0a1c94751c6fb8f4, 0xd0bbeabe359ddd16]; +const ADD_3_6: [u64; 4] = VALS_3; +const SUB_3_6: [u64; 4] = VALS_3; +const MUL_3_6: [u64; 4] = [0, 0, 0, 0]; +const ADD_4_5: [u64; 4] = + [0x9bdecbef06b8f79f, 0xb367f6a249936126, 0x25ad0ad526517e01, 0xf960008629776850]; +const SUB_4_5: [u64; 4] = + [0x4a1fee656a24a023, 0xdb070c7ee420da32, 0x9767e3c2781c2d14, 0x8b22658fd9dc65ef]; +const MUL_4_5: [u64; 4] = + [0xb576898b24ce8b26, 0xfd21ab2dcc5953ea, 0x6cf35cb991b41507, 0x8d202ebbbb3cfb5c]; +const ADD_4_6: [u64; 4] = VALS_4; +const SUB_4_6: [u64; 4] = VALS_4; +const MUL_4_6: [u64; 4] = [0, 0, 0, 0]; +const ADD_5_6: [u64; 4] = VALS_5; +const SUB_5_6: [u64; 4] = VALS_5; +const MUL_5_6: [u64; 4] = [0, 0, 0, 0]; + +const NEG_0: [u64; 4] = + [0xe37fce805c4e8661, 0x4229bf05f998e52e, 0xc14ec46fb997ada8, 0xdc473e15c6dba922]; +const INV_0: [u64; 4] = + [0xb1050be5937e81ae, 0xbd6f1a6f49282e49, 0x728211695c8fa4df, 0x7c5a33c130de7334]; +const NEG_1: [u64; 4] = + [0xe5c2e05843769f55, 0x4263994d52c3d292, 0x7462dbcb1b9a1eaf, 0x68d57b95e9be607e]; +const INV_1: [u64; 4] = + [0xe551f2c212d76de6, 0x74ce654bfd807ed6, 0x4667b16ceb7fbc0d, 0x247a2cc9257ab640]; +const NEG_2: [u64; 4] = + [0xf7dd170c93fcee65, 0xe80396a6f85f3591, 0xc47055e7c8075774, 0x65e219ba7ea1092f]; +const INV_2: [u64; 4] = + [0xdda48d116061f542, 0xdb447add6ba832fe, 0x517b7972a38c436e, 0x5fd58edb3e7c89e9]; +const NEG_3: [u64; 4] = + [0x70523e59f934f04b, 0x48b2f04fcd18f9d6, 0x4c75f77359a12c76, 0x949a595a747eb70a]; +const INV_3: [u64; 4] = + [0x5756b86453511e12, 0x50d7c62b470504f5, 0xbe27831882095df9, 0x8869ace893e5a982]; +const NEG_4: [u64; 4] = + [0x8d00a2d5c791341e, 0xb8c87e706925e253, 0x217588b430c92a74, 0x3dbeccf3fe5618e1]; +const INV_4: [u64; 4] = + [0xb6840a3a64de01e9, 0x9703e1fddd33be42, 0x30d83be306042cc4, 0x246672da71956f4a]; +const NEG_5: [u64; 4] = + [0xd720913b31b5d441, 0x93cf8aef4d46bc85, 0xb8dd6c76a8e55789, 0xc8e13283d8327ed0]; +const INV_5: [u64; 4] = + [0xd542799f6b30bfad, 0x7d06ddeac9155583, 0x6743bc828194d6c9, 0x95520c6c86007aa4]; +const NEG_6: [u64; 4] = [0, 0, 0, 0]; +const INV_6: [u64; 4] = [0, 0, 0, 0]; + +fn fe(limbs: [u64; 4]) -> P256FieldElement { + P256FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5, VALS_6].map(fe); + let add: Vec> = vec![ + vec![ADD_0_1, ADD_0_2, ADD_0_3, ADD_0_4, ADD_0_5, ADD_0_6], + vec![ADD_1_2, ADD_1_3, ADD_1_4, ADD_1_5, ADD_1_6], + vec![ADD_2_3, ADD_2_4, ADD_2_5, ADD_2_6], + vec![ADD_3_4, ADD_3_5, ADD_3_6], + vec![ADD_4_5, ADD_4_6], + vec![ADD_5_6], + ]; + let sub: Vec> = vec![ + vec![SUB_0_1, SUB_0_2, SUB_0_3, SUB_0_4, SUB_0_5, SUB_0_6], + vec![SUB_1_2, SUB_1_3, SUB_1_4, SUB_1_5, SUB_1_6], + vec![SUB_2_3, SUB_2_4, SUB_2_5, SUB_2_6], + vec![SUB_3_4, SUB_3_5, SUB_3_6], + vec![SUB_4_5, SUB_4_6], + vec![SUB_5_6], + ]; + let mul: Vec> = vec![ + vec![MUL_0_1, MUL_0_2, MUL_0_3, MUL_0_4, MUL_0_5, MUL_0_6], + vec![MUL_1_2, MUL_1_3, MUL_1_4, MUL_1_5, MUL_1_6], + vec![MUL_2_3, MUL_2_4, MUL_2_5, MUL_2_6], + vec![MUL_3_4, MUL_3_5, MUL_3_6], + vec![MUL_4_5, MUL_4_6], + vec![MUL_5_6], + ]; + for i in 0..7 { + for j in (i + 1)..7 { + let expected_add = fe(add[i][j - i - 1]); + let expected_sub = fe(sub[i][j - i - 1]); + let expected_mul = fe(mul[i][j - i - 1]); + assert_eq!(vals[i].add(&vals[j]), expected_add, "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), expected_add, "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), expected_sub, "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), expected_mul, "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), expected_mul, "mul is commutative ({j},{i})"); + } + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5, VALS_6]; + let negs = [NEG_0, NEG_1, NEG_2, NEG_3, NEG_4, NEG_5, NEG_6]; + let invs = [INV_0, INV_1, INV_2, INV_3, INV_4, INV_5, INV_6]; + for i in 0..7 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5, VALS_6].map(fe); + for &v in &vals { + assert_eq!(v.add(&P256FieldElement::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&P256FieldElement::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), P256FieldElement::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), P256FieldElement::ZERO, "x + (-x) == 0"); + if v != P256FieldElement::ZERO { + assert_eq!(v.mul(&v.invert()), P256FieldElement::ONE, "x * x^-1 == 1"); + } else { + assert_eq!(v.invert(), P256FieldElement::ZERO, "0^-1 == 0 by convention"); + } + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + // p itself must reduce to 0, and p+1 to 1. + assert_eq!(fe(bouncycastle_ec::p256::P_LIMBS), P256FieldElement::ZERO); + let p_plus_1: [u64; 4] = + [0x0000000000000000, 0x0000000100000000, 0x0000000000000000, 0xffffffff00000001]; + assert_eq!(fe(p_plus_1), P256FieldElement::ONE); + + // the maximum u64 limb pattern (2^256 - 1) must also reduce correctly: + // 2^256 - 1 - p == C - 1 (see the module's reduction constant). + let all_ones = [u64::MAX; 4]; + let expected_c_minus_1 = fe([0, 0xffffffff00000000, 0xffffffffffffffff, 0x00000000fffffffe]); + assert_eq!(fe(all_ones), expected_c_minus_1); +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5, VALS_6] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0), "equal values must compare equal"); + for limb_idx in 0..4 { + let mut other = VALS_0; + other[limb_idx] ^= 1; // flip the lowest bit of just this limb + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } + // and distinct KAT values must never be considered equal to one another + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5, VALS_6]; + for i in 0..vals.len() { + for j in (i + 1)..vals.len() { + assert_ne!(fe(vals[i]), fe(vals[j]), "VALS_{i} must differ from VALS_{j}"); + } + } +} + +/// xorshift64* PRNG, fixed seed: deterministic across runs, needs no external dependency, and +/// exercises far more of the input space than the hand-picked KATs above. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0x243F6A8885A308D3); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != P256FieldElement::ZERO { + assert_eq!(a.mul(&a.invert()), P256FieldElement::ONE, "a * a^-1 == 1"); + } + } +} + +/// Products chosen to drive `reduce`'s five-limb accumulator's top limb across every value +/// it can actually take (1 through 9 here; the bound proved in `reduce`'s own doc comment is +/// 10), so the top-limb fold and the final conditional subtraction are both +/// exercised at their extremes rather than only in the middle of their range. Ordinary +/// pseudorandom operands almost never reach the ends: these were found by searching ~900,000 +/// products, including ones biased towards all-ones high words. Expected values are Python's +/// `(a * b) % p`, computed independently of this crate's arithmetic. +#[test] +fn known_answer_mul_at_the_reduction_bounds() { + // (a, b, a*b mod p), one per reachable top-limb value, ascending. + let cases: [([u64; 4], [u64; 4], [u64; 4]); 9] = [ + // top limb 1 + ( + [0x58fda18e0072b4ba, 0x44018c6757092f67, 0x7062f82104dbadc2, 0x0a7d966209a1d185], + [0x93161ab002c04372, 0x174d12d89b2b20bf, 0xfbb4cfefefa03b81, 0xb7ddc0ed19cf8d01], + [0x935ada40df5ae4bf, 0xd5a16c6f4ca1fef1, 0x9d0f22d2d8914382, 0xe7f90b9f48d9a7b3], + ), + // top limb 2 + ( + [0xfb772112cb65c842, 0x41db6eaa2ea5e194, 0xfbb1efd470621edc, 0x544f46bad48f9cd5], + [0x2649ec804438f53e, 0xfc0d6b01caa16cdf, 0xcdb95778c363568d, 0x0b56828c2c3e364b], + [0x661d2c9be83dc368, 0xfc3f5e38d3070baa, 0x1d1b46edf440737b, 0xda71d6eabd80aff8], + ), + // top limb 3 + ( + [0x751b2b6317cce28e, 0x1009dff7c35e7ae8, 0xbd92d7ea34bb659d, 0xec3b3a0e66f030cc], + [0x33a7a72193341604, 0xca466c5c9ae313e5, 0x5d79d64eee2a1c11, 0x2f953e9ec92f953e], + [0x67f426047d0189bc, 0x7e67171716ad1c00, 0xbd91b78c95d2ae1a, 0xa17ef52678dc1098], + ), + // top limb 4 + ( + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + ), + // top limb 5 + ( + [0xfffffffffffffffe, 0x00000000ffffffff, 0x0000000000000000, 0xffffffff00000001], + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0xfffffffffffffffe, 0x00000000ffffffff, 0x0000000000000000, 0xffffffff00000001], + ), + // top limb 6 + ( + [0xfffffffffffffffe, 0x00000000ffffffff, 0x0000000000000000, 0xffffffff00000001], + [0xfffffffffffffffe, 0x00000000ffffffff, 0x0000000000000000, 0xffffffff00000001], + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + ), + // top limb 7 + ( + [0xdbdfc1e46e11f548, 0xeaaeaebe8602b410, 0x2a8ee1d39908a543, 0xe22f0b7ad81ce33c], + [0x6d297469c7525a7d, 0x43d67cb4d6cbe027, 0xc5e7b049c2440291, 0xe2c9cf87f81b2c63], + [0xf2a043490662d4d6, 0x983a450f93a97874, 0xc30aa67280ab5047, 0x011daf973f427b64], + ), + // top limb 8 + ( + [0xe390dbb7431c878a, 0xc77a6c8a447ed952, 0x9fdc24b00b9c5271, 0xf7328f2aa0d3d780], + [0x659e1b082531fad2, 0xd72cfae78deffcb0, 0x7518c3238bf13cd5, 0xb69a873817fb6eb6], + [0x197970f8313456d2, 0x053282a408a86347, 0x5643727d8ddc2562, 0x3385fd2556779cdf], + ), + // top limb 9 + ( + [0x327b39c1b8049ba2, 0x3fda533bd0c4b27e, 0x650aa1bb8de979f1, 0xf8ef8bd33e88832d], + [0xf458f679da7bef0a, 0x1ef3292153ec1d6a, 0x1c754701380036f3, 0xeb534ed60f05a97a], + [0x8c36a1f722aff890, 0xcde36ea8979072c8, 0xfd71907eebc1a3f8, 0x05085278c330a849], + ), + ]; + for (a, b, expected) in cases { + assert_eq!(fe(a).mul(&fe(b)), fe(expected), "a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(b).mul(&fe(a)), fe(expected), "commuted: a = {a:x?}, b = {b:x?}"); + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE0000000001); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 4], + { + let mut l = [0u64; 4]; + l[0] = 1; + l + }, + [u64::MAX; 4], + { + let mut l = [u64::MAX; 4]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `is_zero` is the mask every exceptional-case select in this crate's point arithmetic keys +/// off (infinity detection, the `H == 0` / `R == 0` same-point and opposite-point cases), and +/// otherwise exercised only through those selects. Both truth values are pinned here, with +/// the nonzero side walked across every limb position so a mask that inspected only some limbs +/// would be caught. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(P256FieldElement::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::p256::P_LIMBS).is_zero().to_bool(), "p reduces to 0"); + assert!(!P256FieldElement::ONE.is_zero().to_bool()); + for limb_idx in 0..4 { + let mut limbs = [0u64; 4]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `add`'s carry-out correction and `sub`'s borrow correction, pinned at the operands that force +/// them: `(p-1) + (p-1)` is the largest sum two canonical elements can form, and `0 - 1` the +/// smallest difference. The expected values are `p - 2` and `p - 1` themselves (computed from +/// `p` in Python), so this is a check of the wrap-around handling, not of the digits of `p`. +#[test] +fn known_answer_add_carry_and_sub_borrow_at_the_extremes() { + let p_minus_1 = + fe([0xfffffffffffffffe, 0x00000000ffffffff, 0x0000000000000000, 0xffffffff00000001]); + let p_minus_2 = + fe([0xfffffffffffffffd, 0x00000000ffffffff, 0x0000000000000000, 0xffffffff00000001]); + let two = fe([0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]); + + assert_eq!(p_minus_1.add(&p_minus_1), p_minus_2, "(p-1) + (p-1) == p-2"); + assert_eq!(p_minus_1.add(&P256FieldElement::ONE), P256FieldElement::ZERO, "(p-1) + 1 == 0"); + assert_eq!(P256FieldElement::ZERO.sub(&P256FieldElement::ONE), p_minus_1, "0 - 1 == p-1"); + assert_eq!(P256FieldElement::ONE.sub(&p_minus_1), two, "1 - (p-1) == 2"); + assert_eq!(p_minus_1.negate(), P256FieldElement::ONE, "-(p-1) == 1"); + assert_eq!(P256FieldElement::ONE.invert(), P256FieldElement::ONE, "1^-1 == 1"); + assert_eq!(p_minus_1.invert(), p_minus_1, "(p-1)^-1 == p-1, since (p-1)^2 == 1"); +} diff --git a/crypto/ec/tests/p256_mul_tests.rs b/crypto/ec/tests/p256_mul_tests.rs new file mode 100644 index 000000000..d59c13dbe --- /dev/null +++ b/crypto/ec/tests/p256_mul_tests.rs @@ -0,0 +1,169 @@ +//! Behavioural tests for [`multiply_point`] (`[k]Q`, constant time in `k`): cross-checked against +//! the fixed-base comb for `Q = G`, against the variable-time wNAF multiplier for a random `Q`, +//! and against `[a k mod n]G` for `Q = [a]G` (three multipliers of independent design computing +//! the same point), plus the edge cases a windowed multiplier can get wrong: `k = 0`, `k = 1`, +//! `k = n-1`, a window of all ones, `Q` the identity, `Q` negated, and `Q` given in a +//! non-canonical-`Z` Jacobian representation. + +use bouncycastle_ec::nat; +use bouncycastle_ec::p256::P256FieldElement; +use bouncycastle_ec::p256_comb::comb_multiply_base_point; +use bouncycastle_ec::p256_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p256_mul::multiply_point; +use bouncycastle_ec::p256_point::P256JacobianPoint; +use bouncycastle_ec::p256_scalar::{N_LIMBS, P256PublicScalar, P256Scalar, P256ScalarField}; +use bouncycastle_ec::p256_wnaf::shamir_multiply; + +const LIMBS: usize = 4; + +fn g() -> P256JacobianPoint { + P256JacobianPoint::from_affine( + P256FieldElement::from_limbs(G_X_LIMBS), + P256FieldElement::from_limbs(G_Y_LIMBS), + ) +} + +fn zero() -> [u64; LIMBS] { + [0; LIMBS] +} + +fn small(v: u64) -> [u64; LIMBS] { + let mut limbs = zero(); + limbs[0] = v; + limbs +} + +fn one() -> [u64; LIMBS] { + small(1) +} + +fn n_minus_1() -> [u64; LIMBS] { + let (limbs, borrow) = nat::sub(&N_LIMBS, &one()); + assert_eq!(borrow, 0); + limbs +} + +/// A pseudorandom value reduced into `[0, n)`, so it is a valid scalar for every multiplier. +fn reduced(limbs: [u64; LIMBS]) -> [u64; LIMBS] { + P256ScalarField::from_limbs(limbs).to_limbs() +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; LIMBS] { + let mut limbs = zero(); + for limb in limbs.iter_mut() { + *limb = self.next_u64(); + } + limbs + } +} + +#[test] +fn k_times_g_agrees_with_the_comb_multiplier() { + let mut rng = Xorshift64(0x9E3779B97F4A7C15); + let mut cases = vec![one(), small(2), small(3), small(15), small(16), small(17)]; + cases.push(n_minus_1()); + let mut n_minus_2 = n_minus_1(); + n_minus_2[0] -= 1; + cases.push(n_minus_2); + for _ in 0..50 { + cases.push(reduced(rng.next_limbs())); + } + for k in cases { + let via_mul = multiply_point(&P256Scalar::from_limbs(k), &g()); + let via_comb = comb_multiply_base_point(&P256Scalar::from_limbs(k)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "k = {k:x?}"); + } +} + +#[test] +fn k_times_q_agrees_with_the_wnaf_multiplier_and_with_the_scalar_field() { + let mut rng = Xorshift64(0xC2B2AE3D27D4EB4F); + for _ in 0..50 { + let a = reduced(rng.next_limbs()); + let k = reduced(rng.next_limbs()); + let q = comb_multiply_base_point(&P256Scalar::from_limbs(a)); + + let via_mul = multiply_point(&P256Scalar::from_limbs(k), &q); + + // [0]G + [k]Q by the variable-time multiplier. + let via_wnaf = shamir_multiply( + &P256PublicScalar::from_limbs(zero()), + &P256PublicScalar::from_limbs(k), + &q, + ); + assert_eq!(via_mul.to_affine(), via_wnaf.to_affine(), "wnaf: a = {a:x?}, k = {k:x?}"); + + // [a k mod n]G by the fixed-base multiplier. + let ak = P256ScalarField::from_limbs(a).mul(&P256ScalarField::from_limbs(k)).to_limbs(); + let via_comb = comb_multiply_base_point(&P256Scalar::from_limbs(ak)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "comb: a = {a:x?}, k = {k:x?}"); + } +} + +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let q = g().double(); + assert!(multiply_point(&P256Scalar::from_limbs(zero()), &q).is_infinity().to_bool()); +} + +#[test] +fn edge_case_k_equals_one_is_q() { + let q = g().double(); + assert_eq!(multiply_point(&P256Scalar::from_limbs(one()), &q).to_affine(), q.to_affine()); +} + +#[test] +fn edge_case_k_equals_n_minus_1_is_minus_q() { + let q = g().double().add(&g()); + let result = multiply_point(&P256Scalar::from_limbs(n_minus_1()), &q); + assert_eq!(result.to_affine(), q.negate().to_affine()); +} + +#[test] +fn edge_case_q_is_infinity() { + let k = reduced(Xorshift64(0x2545F4914F6CDD1D).next_limbs()); + assert!( + multiply_point(&P256Scalar::from_limbs(k), &P256JacobianPoint::INFINITY) + .is_infinity() + .to_bool() + ); +} + +#[test] +fn negating_q_negates_the_result() { + let k = reduced(Xorshift64(0x1234_5678_9ABC_DEF1).next_limbs()); + let q = g().double(); + let kq = multiply_point(&P256Scalar::from_limbs(k), &q); + let k_neg_q = multiply_point(&P256Scalar::from_limbs(k), &q.negate()); + assert_eq!(k_neg_q.to_affine(), kq.negate().to_affine()); +} + +#[test] +fn q_in_a_non_canonical_z_representation_gives_the_same_result() { + // (X, Y, Z) and (λ²X, λ³Y, λZ) are the same affine point; the multiplier must treat them alike. + let k = reduced(Xorshift64(0xDEADBEEFCAFEF00D).next_limbs()); + let q = g().double(); + let lambda = P256FieldElement::from_limbs(small(7)); + let lambda2 = lambda.mul(&lambda); + let lambda3 = lambda2.mul(&lambda); + let scaled = + P256JacobianPoint { x: q.x.mul(&lambda2), y: q.y.mul(&lambda3), z: q.z.mul(&lambda) }; + assert_eq!(scaled.to_affine(), q.to_affine(), "the scaled point is the same point"); + assert_eq!( + multiply_point(&P256Scalar::from_limbs(k), &scaled).to_affine(), + multiply_point(&P256Scalar::from_limbs(k), &q).to_affine() + ); +} diff --git a/crypto/ec/tests/p256_point_tests.rs b/crypto/ec/tests/p256_point_tests.rs new file mode 100644 index 000000000..d778d2786 --- /dev/null +++ b/crypto/ec/tests/p256_point_tests.rs @@ -0,0 +1,211 @@ +//! Known-answer tests for [`P256JacobianPoint`] add/double against small multiples of the P-256 +//! base point `G` (SP 800-186 §3.2.1.3). +//! +//! Expected coordinates were computed independently in Python from the SP 800-186 Appendix A.1.1 +//! affine group law (not from recall, and not by reusing this crate's own point arithmetic): +//! +//! ```text +//! def affine_add(P1, P2): +//! if P1 is None: return P2 +//! if P2 is None: return P1 +//! x1, y1 = P1; x2, y2 = P2 +//! if x1 == x2 and (y1 + y2) % p == 0: return None +//! if P1 == P2: +//! lam = (3*x1*x1 + a) * pow(2*y1, p-2, p) % p +//! else: +//! lam = (y2 - y1) * pow(x2 - x1, p-2, p) % p +//! x3 = (lam*lam - x1 - x2) % p +//! y3 = (lam*(x1 - x3) - y1) % p +//! return (x3, y3) +//! ``` +//! +//! with `P_k = k*G` computed by repeated `affine_add` (double-and-add), for small `k`. + +use bouncycastle_ec::p256::P256FieldElement; +use bouncycastle_ec::p256_point::P256JacobianPoint; + +const P1_X: [u64; 4] = + [0xf4a13945d898c296, 0x77037d812deb33a0, 0xf8bce6e563a440f2, 0x6b17d1f2e12c4247]; +const P1_Y: [u64; 4] = + [0xcbb6406837bf51f5, 0x2bce33576b315ece, 0x8ee7eb4a7c0f9e16, 0x4fe342e2fe1a7f9b]; +const P2_X: [u64; 4] = + [0xa60b48fc47669978, 0xc08969e277f21b35, 0x8a52380304b51ac3, 0x7cf27b188d034f7e]; +const P2_Y: [u64; 4] = + [0x9e04b79d227873d1, 0xba7dade63ce98229, 0x293d9ac69f7430db, 0x07775510db8ed040]; +const P3_X: [u64; 4] = + [0xfb41661bc6e7fd6c, 0xe6c6b721efada985, 0xc8f7ef951d4bf165, 0x5ecbe4d1a6330a44]; +const P3_Y: [u64; 4] = + [0x9a79b127a27d5032, 0xd82ab036384fb83d, 0x374b06ce1a64a2ec, 0x8734640c4998ff7e]; +const P4_X: [u64; 4] = + [0x509302446b030852, 0x031fe2db785596ef, 0xa02dde659ee62bd0, 0xe2534a3532d08fbb]; +const P4_Y: [u64; 4] = + [0x5c42c23f184ed8c6, 0x4efc96c3f30ee005, 0x19dfee5fda862d76, 0xe0f1575a4c633cc7]; +const P5_X: [u64; 4] = + [0x21554a0dc3d033ed, 0xef8c82fd1f5be524, 0xd784c85608668fdf, 0x51590b7a515140d2]; +const P5_Y: [u64; 4] = + [0xd1d0bb44fda16da4, 0x0d012f00d4d80888, 0x8ae1bf36bf8a7926, 0xe0c17da8904a727d]; +const P7_X: [u64; 4] = + [0x300628703187b2a3, 0x7ef9f8b8a80fef5b, 0x25bb30667c01fb60, 0x8e533b6fa0bf7b46]; +const P7_Y: [u64; 4] = + [0xc55e1a86c1f400b4, 0x53c73633cb041b21, 0x6d069f83a6f59000, 0x73eb1dbde0331836]; +const P11_X: [u64; 4] = + [0x433391d374bc21d1, 0x16742ed0255048bf, 0x0638379db0c21cda, 0x3ed113b7883b4c59]; +const P11_Y: [u64; 4] = + [0xe2f8eefce82a3740, 0x090d04da5e9889da, 0x24c843afa4f4c68a, 0x9099209accc4c8a2]; +const P13_X: [u64; 4] = + [0x98e15d9d46072c01, 0x792e284b65ead58a, 0x61805df2d85ee2fc, 0x177c837ae0ac495a]; +const P13_Y: [u64; 4] = + [0x9c43bbe2efc7bfd8, 0x26ee14c3a1fb4df3, 0xa24091adb40f4e72, 0x63bb58cd4ebea558]; + +const SUM_1_2_X: [u64; 4] = P3_X; +const SUM_1_2_Y: [u64; 4] = P3_Y; +const SUM_2_3_X: [u64; 4] = P5_X; +const SUM_2_3_Y: [u64; 4] = P5_Y; +const SUM_1_3_X: [u64; 4] = P4_X; +const SUM_1_3_Y: [u64; 4] = P4_Y; +const SUM_4_5_X: [u64; 4] = + [0xd79e8a4b90949ee0, 0x9e0acb8c2c6df8b3, 0x878938d51d71f872, 0xea68d7b6fedf0b71]; +const SUM_4_5_Y: [u64; 4] = + [0xe85a224a4dd048fa, 0x4d714feaa4de823f, 0x87014a964a8ea0c8, 0x2a2744c972c9fce7]; +const SUM_1_4_X: [u64; 4] = P5_X; +const SUM_1_4_Y: [u64; 4] = P5_Y; +const SUM_7_11_X: [u64; 4] = + [0xd936266dbd781fda, 0x37bb4c6f9ea55c63, 0xdefc9378d1c7c874, 0x1057e0ab5780f470]; +const SUM_7_11_Y: [u64; 4] = + [0x8d83b3393c6a45a2, 0xdcc11b5c5ef4f1f7, 0x9fa9b7dfd96ee5a7, 0xf6f1645a15cbe5dc]; +const SUM_2_13_X: [u64; 4] = + [0x63668c63e59b9d5f, 0xae03af92de3a0ef1, 0xadfb378999888265, 0xf0454dc6971abae7]; +const SUM_2_13_Y: [u64; 4] = + [0x47e59cde0d034f36, 0x2a3b21ce75b5fa3f, 0x4e6594e51f9643e6, 0xb5b93ee3592e2d1f]; + +const DBL_1_X: [u64; 4] = P2_X; +const DBL_1_Y: [u64; 4] = P2_Y; +const DBL_2_X: [u64; 4] = P4_X; +const DBL_2_Y: [u64; 4] = P4_Y; +const DBL_3_X: [u64; 4] = + [0xc6b0aae93c2291a9, 0x024c740debb215b4, 0x92d3242cb897dde3, 0xb01a172a76a4602c]; +const DBL_3_Y: [u64; 4] = + [0xfd7c48538fc77fe2, 0x1c00f7701c7e16bd, 0x6fec0e2dfba70379, 0xe85c10743237dad5]; +const DBL_4_X: [u64; 4] = + [0xb4dd9dc1db6fb393, 0xc1d238980fce97db, 0x4042742d3ab54cad, 0x62d9779dbee9b053]; +const DBL_4_Y: [u64; 4] = + [0xda540a6a0f09957e, 0xa2ed51f6bbe76a78, 0x4ff15d771167cee0, 0xad5accbd91e9d824]; +const DBL_5_X: [u64; 4] = + [0x4c36069404c5723f, 0x45ca6c471c48306e, 0x591214d1ea223fb5, 0xcef66d6b2a3a993e]; +const DBL_5_Y: [u64; 4] = + [0xca34bbaa44af0773, 0x590ded29fe751eee, 0x6e123cdd9d3b4c10, 0x878662a229aaae90]; +const DBL_7_X: [u64; 4] = + [0x5709277324d2920b, 0xf126acbe7a069c5e, 0x7a76647f4336df3c, 0x54e77a001c3862b9]; +const DBL_7_Y: [u64; 4] = + [0x1ba7c82f60d0b375, 0x7171ea7773509008, 0x42121f8c05a2e7c3, 0xf599f1bb29f43175]; + +fn fe(limbs: [u64; 4]) -> P256FieldElement { + P256FieldElement::from_limbs(limbs) +} + +fn pt(x: [u64; 4], y: [u64; 4]) -> P256JacobianPoint { + P256JacobianPoint::from_affine(fe(x), fe(y)) +} + +fn assert_affine_eq(p: &P256JacobianPoint, x: [u64; 4], y: [u64; 4], msg: &str) { + let (ax, ay) = p.to_affine().unwrap_or_else(|| panic!("{msg}: unexpectedly at infinity")); + assert_eq!(ax, fe(x), "{msg}: x mismatch"); + assert_eq!(ay, fe(y), "{msg}: y mismatch"); +} + +#[test] +fn known_answer_add() { + let cases: [(([u64; 4], [u64; 4]), ([u64; 4], [u64; 4]), ([u64; 4], [u64; 4])); 7] = [ + ((P1_X, P1_Y), (P2_X, P2_Y), (SUM_1_2_X, SUM_1_2_Y)), + ((P2_X, P2_Y), (P3_X, P3_Y), (SUM_2_3_X, SUM_2_3_Y)), + ((P1_X, P1_Y), (P3_X, P3_Y), (SUM_1_3_X, SUM_1_3_Y)), + ((P4_X, P4_Y), (P5_X, P5_Y), (SUM_4_5_X, SUM_4_5_Y)), + ((P1_X, P1_Y), (P4_X, P4_Y), (SUM_1_4_X, SUM_1_4_Y)), + ((P7_X, P7_Y), (P11_X, P11_Y), (SUM_7_11_X, SUM_7_11_Y)), + ((P2_X, P2_Y), (P13_X, P13_Y), (SUM_2_13_X, SUM_2_13_Y)), + ]; + for (p1, p2, expected) in cases { + let a = pt(p1.0, p1.1); + let b = pt(p2.0, p2.1); + assert_affine_eq(&a.add(&b), expected.0, expected.1, "add"); + assert_affine_eq(&b.add(&a), expected.0, expected.1, "add is commutative"); + } +} + +#[test] +fn known_answer_double() { + let cases: [(([u64; 4], [u64; 4]), ([u64; 4], [u64; 4])); 6] = [ + ((P1_X, P1_Y), (DBL_1_X, DBL_1_Y)), + ((P2_X, P2_Y), (DBL_2_X, DBL_2_Y)), + ((P3_X, P3_Y), (DBL_3_X, DBL_3_Y)), + ((P4_X, P4_Y), (DBL_4_X, DBL_4_Y)), + ((P5_X, P5_Y), (DBL_5_X, DBL_5_Y)), + ((P7_X, P7_Y), (DBL_7_X, DBL_7_Y)), + ]; + for (p, expected) in cases { + let a = pt(p.0, p.1); + assert_affine_eq(&a.double(), expected.0, expected.1, "double"); + // add() must dispatch to the same result when asked to add a point to itself. + assert_affine_eq(&a.add(&a), expected.0, expected.1, "add(P, P) == double(P)"); + } +} + +#[test] +fn point_plus_negation_is_infinity() { + for (x, y) in [(P1_X, P1_Y), (P2_X, P2_Y), (P7_X, P7_Y), (P13_X, P13_Y)] { + let p = pt(x, y); + let neg = p.negate(); + assert!(p.add(&neg).to_affine().is_none(), "P + (-P) must be infinity"); + assert!(neg.add(&p).to_affine().is_none(), "-P + P must be infinity"); + } +} + +#[test] +fn infinity_identities() { + let inf = P256JacobianPoint::INFINITY; + assert!(inf.is_infinity().to_bool()); + assert!(inf.to_affine().is_none()); + assert!(inf.add(&inf).to_affine().is_none(), "infinity + infinity == infinity"); + + for (x, y) in [(P1_X, P1_Y), (P7_X, P7_Y), (P13_X, P13_Y)] { + let p = pt(x, y); + assert_affine_eq(&p.add(&inf), x, y, "P + infinity == P"); + assert_affine_eq(&inf.add(&p), x, y, "infinity + P == P"); + } +} + +#[test] +fn scaled_z_representation_still_adds_correctly() { + // A point need not be affine (Z == 1) to be added correctly -- P1 scaled by an arbitrary + // nonzero Z' must still represent the same affine point through add(). + let z_scale = + fe([0x1122334455667788, 0x99aabbccddeeff00, 0x0f0e0d0c0b0a0908, 0x0102030405060708]); + let z2 = z_scale.mul(&z_scale); + let z3 = z2.mul(&z_scale); + let scaled_p1 = P256JacobianPoint { x: fe(P1_X).mul(&z2), y: fe(P1_Y).mul(&z3), z: z_scale }; + + // sanity: the scaled point still maps back to the same affine coordinates + assert_affine_eq(&scaled_p1, P1_X, P1_Y, "scaled P1 affine round-trip"); + + let p2 = pt(P2_X, P2_Y); + assert_affine_eq(&scaled_p1.add(&p2), SUM_1_2_X, SUM_1_2_Y, "scaled-Z add"); + assert_affine_eq(&scaled_p1.double(), DBL_1_X, DBL_1_Y, "scaled-Z double"); +} + +/// `is_infinity` was previously only ever asserted true. The false side is pinned on the base +/// point and on values derived from it, and doubling the identity must stay at the identity +/// (`double` is total, per its docs, and the comb multiplier's accumulator starts there). +#[test] +fn is_infinity_is_false_for_finite_points_and_infinity_doubles_to_itself() { + let g = pt(bouncycastle_ec::p256_domain::G_X_LIMBS, bouncycastle_ec::p256_domain::G_Y_LIMBS); + assert!(!g.is_infinity().to_bool(), "G is not the identity"); + assert!(!g.double().is_infinity().to_bool(), "2G is not the identity"); + assert!(!g.negate().is_infinity().to_bool(), "-G is not the identity"); + assert!(!g.add(&g.double()).is_infinity().to_bool(), "3G is not the identity"); + + let inf = P256JacobianPoint::INFINITY; + assert!(inf.double().is_infinity().to_bool(), "2 * infinity == infinity"); + assert!(inf.double().to_affine().is_none()); + assert!(inf.negate().is_infinity().to_bool(), "-infinity == infinity"); + assert!(inf.double().add(&g).to_affine().is_some(), "2 * infinity + G == G, a finite point"); +} diff --git a/crypto/ec/tests/p256_scalar_tests.rs b/crypto/ec/tests/p256_scalar_tests.rs new file mode 100644 index 000000000..6ae25982f --- /dev/null +++ b/crypto/ec/tests/p256_scalar_tests.rs @@ -0,0 +1,459 @@ +//! Known-answer tests for [`P256ScalarField`] arithmetic mod the P-256 curve order `n`. +//! +//! No NIST-published KAT exists for raw scalar-field arithmetic (only for signatures, which this +//! crate doesn't produce yet). Expected values were computed independently in Python: +//! +//! ```text +//! n = int("ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", 16) +//! random.seed(99) +//! vals = [random.randrange(1, n) for _ in range(5)] + [0] +//! # for each pair (i, j) with i < j: (vals[i]+vals[j]) % n, (vals[i]-vals[j]) % n, (vals[i]*vals[j]) % n +//! # for each i: (-vals[i]) % n, pow(vals[i], n-2, n) if vals[i] != 0 else 0 +//! ``` + +use bouncycastle_ec::p256_scalar::{N_LIMBS, P256PublicScalar, P256Scalar, P256ScalarField}; + +const VALS_0: [u64; 4] = + [0x61790134676b1b6a, 0x9974d75b333824fe, 0x3af27f802dc5fd3d, 0x221c4e003f9931ee]; +const VALS_1: [u64; 4] = + [0x162a01dec28753f9, 0xbaa1c6f1404b6eaf, 0x87e355b26210b784, 0xb35331ceaf2ed9dd]; +const VALS_2: [u64; 4] = + [0x16ff82e389e3995b, 0x9fb932d4f0397722, 0x331057ca7d411fab, 0xb8e3c71f6bf08d62]; +const VALS_3: [u64; 4] = + [0x96a1da2c9cfbba44, 0xcae8c077377925b3, 0xc9cf158de6e96d45, 0xd283eb3a5fbd238e]; +const VALS_4: [u64; 4] = + [0xdc8ac0bb635b4c42, 0x366c5acdaeafb905, 0x7623c4dd26fb984f, 0x2dd301c8a91afa5c]; +const VALS_5: [u64; 4] = + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; + +const ADD_0_1: [u64; 4] = + [0x77a3031329f26f63, 0x54169e4c738393ad, 0xc2d5d5328fd6b4c2, 0xd56f7fceeec80bcb]; +const SUB_0_1: [u64; 4] = + [0x3f08ca18a146ecc2, 0x9bba0b179a0454d4, 0xb30f29cdcbb545b8, 0x6ec91c30906a5811]; +const MUL_0_1: [u64; 4] = + [0x9b957490b0b957fd, 0x363308dfb20a5e88, 0xc666ada84f2ce72c, 0xe85ab26303d4786c]; +const ADD_0_2: [u64; 4] = + [0x78788417f14eb4c5, 0x392e0a3023719c20, 0x6e02d74aab071ce9, 0xdb00151fab89bf50]; +const SUB_0_2: [u64; 4] = + [0x3e334913d9eaa760, 0xb6a29f33ea164c61, 0x07e227b5b084dd91, 0x693886dfd3a8a48d]; +const MUL_0_2: [u64; 4] = + [0xe0dcb929e7d8df8e, 0x7d7515aa7a46229e, 0x06d1248dded29839, 0x418877fe0e82e6a6]; +const ADD_0_3: [u64; 4] = + [0xf81adb610466d5ae, 0x645d97d26ab14ab1, 0x04c1950e14af6a83, 0xf4a0393a9f56557d]; +const SUB_0_3: [u64; 4] = + [0xbe90f1cac6d28677, 0x8b731191a2d69dcf, 0x712369f246dc8ff7, 0x4f9862c4dfdc0e60]; +const MUL_0_3: [u64; 4] = + [0xa9178f540ad740bc, 0xedda23c9d3d4b992, 0xdbb6d4ed821c6d0c, 0xc550c0b372ace280]; +const ADD_0_4: [u64; 4] = + [0x3e03c1efcac667ac, 0xcfe13228e1e7de04, 0xb116445d54c1958c, 0x4fef4fc8e8b42c4a]; +const SUB_0_4: [u64; 4] = + [0x78a80b3c0072f479, 0x1fef773b2ba00a7d, 0xc4cebaa306ca64ee, 0xf4494c36967e3792]; +const MUL_0_4: [u64; 4] = + [0xdc0ee445deb72aeb, 0xd73d662d513120c8, 0x9733e6b7ee202567, 0x5d5546a40a8b6232]; +const ADD_0_5: [u64; 4] = VALS_0; +const SUB_0_5: [u64; 4] = VALS_0; +const MUL_0_5: [u64; 4] = [0, 0, 0, 0]; +const ADD_1_2: [u64; 4] = + [0x396fb9ff5007c803, 0x9d73ff18896d474c, 0xbaf3ad7cdf51d730, 0x6c36f8ef1b1f673e]; +const SUB_1_2: [u64; 4] = + [0xf2e449be3506dfef, 0xd7cf8ec9f7299611, 0x54d2fde7e4cf97d8, 0xfa6f6aae433e4c7c]; +const MUL_1_2: [u64; 4] = + [0xdf68307f79c99226, 0xd8af071df3bc3ab4, 0x5032a9e74ba6242e, 0x68e98c9aeca29515]; +const ADD_1_3: [u64; 4] = + [0xb9121148631fe8ec, 0xc8a38cbad0acf5dd, 0x51b26b4048fa24ca, 0x85d71d0a0eebfd6b]; +const SUB_1_3: [u64; 4] = + [0x7341f27521eebf06, 0xaca00127afe9e780, 0xbe1440247b274a3e, 0xe0cf46934f71b64f]; +const MUL_1_3: [u64; 4] = + [0xbcde5301d8a0ab88, 0x0b8706edf8da5fd4, 0xd38a80b07a8d3dc5, 0x0eec5530af2b75a3]; +const ADD_1_4: [u64; 4] = + [0xf2b4c29a25e2a03b, 0xf10e21beeefb27b4, 0xfe071a8f890c4fd3, 0xe12633975849d439]; +const SUB_1_4: [u64; 4] = + [0x399f41235f2c07b7, 0x84356c23919bb5a9, 0x11bf90d53b151f35, 0x858030060613df81]; +const MUL_1_4: [u64; 4] = + [0x41ce6159e83eec06, 0x95dadd4fd50ec42d, 0x7aa12baaa84bd66f, 0x380adf3eea5fe1ed]; +const ADD_1_5: [u64; 4] = VALS_1; +const SUB_1_5: [u64; 4] = VALS_1; +const MUL_1_5: [u64; 4] = [0, 0, 0, 0]; +const ADD_2_3: [u64; 4] = + [0xb9e7924d2a7c2e4e, 0xadbaf89e809afe50, 0xfcdf6d58642a8cf1, 0x8b67b25acbadb0ef]; +const SUB_2_3: [u64; 4] = + [0x74177379e94b0468, 0x91b76d0b5fd7eff3, 0x6941423c9657b265, 0xe65fdbe40c3369d4]; +const MUL_2_3: [u64; 4] = + [0x1aabe983764c72ee, 0x360daf66df8a2dd9, 0x9c5bf9e6ed6fc2ef, 0x5a5175ed5133aa90]; +const ADD_2_4: [u64; 4] = + [0xf38a439eed3ee59d, 0xd6258da29ee93027, 0xa9341ca7a43cb7fa, 0xe6b6c8e8150b87be]; +const SUB_2_4: [u64; 4] = + [0x3a74c22826884d19, 0x694cd8074189be1c, 0xbcec92ed5645875c, 0x8b10c556c2d59305]; +const MUL_2_4: [u64; 4] = + [0x789cb76159b4f968, 0x3a51a7a176c4a3c5, 0xe6edf097f54c0ce4, 0xb6577b86fafd724c]; +const ADD_2_5: [u64; 4] = VALS_2; +const SUB_2_5: [u64; 4] = VALS_2; +const MUL_2_5: [u64; 4] = [0, 0, 0, 0]; +const ADD_3_4: [u64; 4] = + [0x7f72d02503f3e135, 0x446e20973f114034, 0x3ff2da6b0de50595, 0x0056ed0408d81dea]; +const SUB_3_4: [u64; 4] = + [0xba17197139a06e02, 0x947c65a988c96cad, 0x53ab50b0bfedd4f6, 0xa4b0e971b6a22932]; +const MUL_3_4: [u64; 4] = + [0x0d92f75860a0f3db, 0xb6cf9a4444fab683, 0x8596eeb5b6fa4cdc, 0xf3cdc06731082412]; +const ADD_3_5: [u64; 4] = VALS_3; +const SUB_3_5: [u64; 4] = VALS_3; +const MUL_3_5: [u64; 4] = [0, 0, 0, 0]; +const ADD_4_5: [u64; 4] = VALS_4; +const SUB_4_5: [u64; 4] = VALS_4; +const MUL_4_5: [u64; 4] = [0, 0, 0, 0]; + +const NEG_0: [u64; 4] = + [0x9240c98e94f809e7, 0x2372235273df7986, 0xc50d807fd23a02c2, 0xdde3b1fec066ce12]; +const INV_0: [u64; 4] = + [0x089842e0ace7c3d2, 0xfb2f2003985af5a0, 0x1e6d949e78e789fd, 0x74836b0dc09b705f]; +const NEG_1: [u64; 4] = + [0xdd8fc8e439dbd158, 0x024533bc66cc2fd5, 0x781caa4d9def487b, 0x4cacce3050d12623]; +const INV_1: [u64; 4] = + [0xf20be0b54e165e3a, 0xe05a8fadd40727b1, 0x8ef6950b8f2a8ba9, 0x04bd89fcbf71beec]; +const NEG_2: [u64; 4] = + [0xdcba47df727f8bf6, 0x1d2dc7d8b6de2762, 0xccefa83582bee054, 0x471c38df940f729e]; +const INV_2: [u64; 4] = + [0xd8ca063d9c9c1d74, 0x861819f986ae0cd4, 0x79070aae69e37c0b, 0x901e883cc3fb5f6a]; +const NEG_3: [u64; 4] = + [0x5d17f0965f676b0d, 0xf1fe3a366f9e78d1, 0x3630ea72191692b9, 0x2d7c14c4a042dc72]; +const INV_3: [u64; 4] = + [0xf116a77d8f241b5f, 0xefe7cb74c63638a3, 0xa1e0b03b62331768, 0xec7a0765c915659d]; +const NEG_4: [u64; 4] = + [0x172f0a079907d90f, 0x867a9fdff867e57f, 0x89dc3b22d90467b0, 0xd22cfe3656e505a4]; +const INV_4: [u64; 4] = + [0x627206b591d2b552, 0xea98323796b958c5, 0x8af6cfdf19ae033d, 0x6a1249e2f5e13abe]; +const NEG_5: [u64; 4] = [0, 0, 0, 0]; +const INV_5: [u64; 4] = [0, 0, 0, 0]; + +fn fe(limbs: [u64; 4]) -> P256ScalarField { + P256ScalarField::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5].map(fe); + let add: Vec> = vec![ + vec![ADD_0_1, ADD_0_2, ADD_0_3, ADD_0_4, ADD_0_5], + vec![ADD_1_2, ADD_1_3, ADD_1_4, ADD_1_5], + vec![ADD_2_3, ADD_2_4, ADD_2_5], + vec![ADD_3_4, ADD_3_5], + vec![ADD_4_5], + ]; + let sub: Vec> = vec![ + vec![SUB_0_1, SUB_0_2, SUB_0_3, SUB_0_4, SUB_0_5], + vec![SUB_1_2, SUB_1_3, SUB_1_4, SUB_1_5], + vec![SUB_2_3, SUB_2_4, SUB_2_5], + vec![SUB_3_4, SUB_3_5], + vec![SUB_4_5], + ]; + let mul: Vec> = vec![ + vec![MUL_0_1, MUL_0_2, MUL_0_3, MUL_0_4, MUL_0_5], + vec![MUL_1_2, MUL_1_3, MUL_1_4, MUL_1_5], + vec![MUL_2_3, MUL_2_4, MUL_2_5], + vec![MUL_3_4, MUL_3_5], + vec![MUL_4_5], + ]; + for i in 0..6 { + for j in (i + 1)..6 { + let expected_add = fe(add[i][j - i - 1]); + let expected_sub = fe(sub[i][j - i - 1]); + let expected_mul = fe(mul[i][j - i - 1]); + assert_eq!(vals[i].add(&vals[j]), expected_add, "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), expected_add, "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), expected_sub, "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), expected_mul, "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), expected_mul, "mul is commutative ({j},{i})"); + } + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5]; + let negs = [NEG_0, NEG_1, NEG_2, NEG_3, NEG_4, NEG_5]; + let invs = [INV_0, INV_1, INV_2, INV_3, INV_4, INV_5]; + for i in 0..6 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5].map(fe); + for &v in &vals { + assert_eq!(v.add(&P256ScalarField::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&P256ScalarField::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), P256ScalarField::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), P256ScalarField::ZERO, "x + (-x) == 0"); + if v != P256ScalarField::ZERO { + assert_eq!(v.mul(&v.invert()), P256ScalarField::ONE, "x * x^-1 == 1"); + } else { + assert_eq!(v.invert(), P256ScalarField::ZERO, "0^-1 == 0 by convention"); + } + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..4 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + assert_eq!(fe(N_LIMBS), P256ScalarField::ZERO); + let n_plus_1: [u64; 4] = + [0xf3b9cac2fc632552, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000]; + assert_eq!(fe(n_plus_1), P256ScalarField::ONE); +} + +#[test] +fn public_scalar_round_trips_and_reduces() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + assert_eq!(P256PublicScalar::from_limbs(limbs).to_limbs(), limbs); + } + // out-of-range input must reduce, exactly like P256ScalarField::from_limbs. + assert_eq!(P256PublicScalar::from_limbs(N_LIMBS).to_limbs(), [0, 0, 0, 0]); + let n_plus_1: [u64; 4] = + [0xf3b9cac2fc632552, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000]; + assert_eq!(P256PublicScalar::from_limbs(n_plus_1).to_limbs(), [1, 0, 0, 0]); +} + +#[test] +fn public_scalar_eq_detects_a_difference_in_any_limb() { + let base = P256PublicScalar::from_limbs(VALS_0); + assert_eq!(base, P256PublicScalar::from_limbs(VALS_0)); + for limb_idx in 0..4 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!( + base, + P256PublicScalar::from_limbs(other), + "a difference in limb {limb_idx} must be detected" + ); + } +} + +#[test] +fn secret_scalar_be_bytes_round_trip() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4] { + let secret = P256Scalar::from_limbs(limbs); + let round_tripped = P256Scalar::from_be_bytes(&secret.to_be_bytes()); + assert_eq!(round_tripped, secret); + } +} + +#[test] +fn secret_scalar_from_be_bytes_reduces_out_of_range_input() { + let mut n_bytes = [0u8; 32]; + for (i, limb) in N_LIMBS.iter().rev().enumerate() { + n_bytes[i * 8..i * 8 + 8].copy_from_slice(&limb.to_be_bytes()); + } + assert_eq!(P256Scalar::from_be_bytes(&n_bytes), P256Scalar::from_limbs([0, 0, 0, 0])); +} + +#[test] +fn scalar_field_from_secret_matches_from_limbs() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + let secret = P256Scalar::from_limbs(limbs); + assert_eq!(P256ScalarField::from_secret(&secret), fe(limbs)); + } +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property test. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0x9E3779B97F4A7C15); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != P256ScalarField::ZERO { + assert_eq!(a.mul(&a.invert()), P256ScalarField::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE0000000009); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 4], + { + let mut l = [0u64; 4]; + l[0] = 1; + l + }, + [u64::MAX; 4], + { + let mut l = [u64::MAX; 4]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `zeroize` is the signing path's way of not leaving `d`, `k` or `k^-1` legible on the stack +/// (see the method's own docs for why this type is not wrapped in `Secret` instead). Whether the +/// volatile write survives optimization cannot be observed from Rust; that the value is actually +/// cleared can be, and is what would break if the method were ever reduced to a no-op. +#[test] +fn zeroize_clears_the_value() { + let mut a = fe([1, 2, 3, 4]); + assert_ne!(a, P256ScalarField::ZERO, "precondition: the value starts non-zero"); + a.zeroize(); + assert_eq!(a, P256ScalarField::ZERO, "zeroize must leave the value at zero"); + + // and it is idempotent, so a caller scrubbing twice on overlapping paths is harmless + a.zeroize(); + assert_eq!(a, P256ScalarField::ZERO); +} + +/// `is_zero` on the scalar field, pinned for both truth values and across every limb position +/// (see the field test of the same name for why); it is the check behind `negate`'s `0 -> 0` +/// special case. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(P256ScalarField::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::p256_scalar::N_LIMBS).is_zero().to_bool(), "n reduces to 0"); + assert!(!P256ScalarField::ONE.is_zero().to_bool()); + for limb_idx in 0..4 { + let mut limbs = [0u64; 4]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `n - 1`, the largest canonical scalar, is the worst case for REDC's final conditional +/// subtraction and for `add`'s carry correction, and was absent from every known-answer set +/// above (whose values are pseudorandom, so never near `n`). Expected values follow from +/// `(n-1)^2 == 1` and `(n-1) + (n-1) == n - 2` in the field, computed from `n` in Python. +#[test] +fn known_answer_at_n_minus_1() { + let n_minus_1_limbs: [u64; 4] = + [0xf3b9cac2fc632550, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000]; + let n_minus_1 = fe(n_minus_1_limbs); + let n_minus_2 = + fe([0xf3b9cac2fc63254f, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000]); + + assert_eq!(n_minus_1.to_limbs(), n_minus_1_limbs, "Montgomery round trip of n-1"); + assert_eq!(P256PublicScalar::from_limbs(n_minus_1_limbs).to_limbs(), n_minus_1_limbs); + assert_eq!(n_minus_1.add(&n_minus_1), n_minus_2, "(n-1) + (n-1) == n-2"); + assert_eq!(n_minus_1.add(&P256ScalarField::ONE), P256ScalarField::ZERO, "(n-1) + 1 == 0"); + assert_eq!(P256ScalarField::ZERO.sub(&P256ScalarField::ONE), n_minus_1, "0 - 1 == n-1"); + assert_eq!(n_minus_1.mul(&n_minus_1), P256ScalarField::ONE, "(n-1)^2 == 1"); + assert_eq!(n_minus_1.square(), P256ScalarField::ONE, "(n-1)^2 == 1, via square"); + assert_eq!(n_minus_1.negate(), P256ScalarField::ONE, "-(n-1) == 1"); + assert_eq!(n_minus_1.invert(), n_minus_1, "(n-1)^-1 == n-1"); + assert_eq!(P256ScalarField::ONE.invert(), P256ScalarField::ONE, "1^-1 == 1"); +} + +/// The variable-time inverse on the public scalar type must agree with the constant-time +/// [`P256ScalarField::invert`] everywhere: on the known-answer values (whose inverses are pinned above +/// from Python), on `0` (both return `0` by convention), `1`, `n - 1`, and on pseudorandom +/// values -- including ones with long runs of zero bits, which are where the binary algorithm +/// takes its longest halving chains. +#[test] +fn public_scalar_invert_vartime_agrees_with_constant_time_invert() { + let n_minus_1 = { + let mut l = bouncycastle_ec::p256_scalar::N_LIMBS; + l[0] -= 1; + l + }; + let mut one = [0u64; 4]; + one[0] = 1; + // Values whose low limb is exactly 1 with higher limbs set: odd, so the binary algorithm must + // not halve them, yet indistinguishable from `1` by their low limb alone. A halving test that + // looked at more than the lowest bit (or at the wrong limb) breaks the `u == x1 * a` invariant + // on exactly these and nowhere the pseudorandom inputs below would reach. + let mut low_limb_one_a = [0u64; 4]; + low_limb_one_a[0] = 1; + low_limb_one_a[1] = 1; + let mut low_limb_one_b = [0u64; 4]; + low_limb_one_b[0] = 1; + low_limb_one_b[3] = 1; + let mut low_limb_one_c = [0u64; 4]; + low_limb_one_c[0] = 1; + low_limb_one_c[1] = u64::MAX; + let mut fixed: Vec<[u64; 4]> = + vec![[0u64; 4], one, n_minus_1, low_limb_one_a, low_limb_one_b, low_limb_one_c]; + fixed.extend([VALS_0, VALS_1, VALS_2]); + for limbs in fixed { + assert_eq!( + bouncycastle_ec::p256_scalar::P256PublicScalar::from_limbs(limbs).invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } + assert_eq!( + bouncycastle_ec::p256_scalar::P256PublicScalar::from_limbs([0u64; 4]).invert_vartime(), + P256ScalarField::ZERO, + "0^-1 == 0 by convention, matching invert" + ); + + let mut rng = Xorshift64(0x1AC0_B5EC_0000_0004); + for i in 0..500 { + let mut limbs = rng.next_limbs(); + if i % 4 == 0 { + // Sparse values: clear the low half of each limb so `u`/`v` shed many bits per step. + for limb in limbs.iter_mut() { + *limb &= 0xffff_ffff_0000_0000; + } + } + assert_eq!( + bouncycastle_ec::p256_scalar::P256PublicScalar::from_limbs(limbs).invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } +} diff --git a/crypto/ec/tests/p256_sec1_tests.rs b/crypto/ec/tests/p256_sec1_tests.rs new file mode 100644 index 000000000..179c179aa --- /dev/null +++ b/crypto/ec/tests/p256_sec1_tests.rs @@ -0,0 +1,143 @@ +//! Known-answer tests for SEC 1 §2.3.3/§2.3.4 point encoding and decoding. +//! +//! `G` and `3G`'s coordinates come from `p256_comb_tests.rs`'s already-verified values (in turn +//! from the SP 800-186 Appendix A.1.1 affine group law); the encodings below were derived from +//! those coordinates independently in Python via plain big-endian byte packing: +//! `bytes([tag]) + x.to_bytes(32, 'big') + (y.to_bytes(32, 'big') if uncompressed else b'')`. +//! `G` has odd `y` (tag `0x03`) and `3G` has even `y` (tag `0x02`), so between them both +//! compressed tags are exercised. + +use bouncycastle_ec::p256::P256FieldElement; +use bouncycastle_ec::p256_sec1::{decode, decode_point, encode_compressed, encode_uncompressed}; + +const G_X: [u64; 4] = + [0xf4a13945d898c296, 0x77037d812deb33a0, 0xf8bce6e563a440f2, 0x6b17d1f2e12c4247]; +const G_Y: [u64; 4] = + [0xcbb6406837bf51f5, 0x2bce33576b315ece, 0x8ee7eb4a7c0f9e16, 0x4fe342e2fe1a7f9b]; +const P3_X: [u64; 4] = + [0xfb41661bc6e7fd6c, 0xe6c6b721efada985, 0xc8f7ef951d4bf165, 0x5ecbe4d1a6330a44]; +const P3_Y: [u64; 4] = + [0x9a79b127a27d5032, 0xd82ab036384fb83d, 0x374b06ce1a64a2ec, 0x8734640c4998ff7e]; + +#[rustfmt::skip] +const UNCOMPRESSED_G: [u8; 65] = [ + 0x04, 0x6b, 0x17, 0xd1, 0xf2, 0xe1, 0x2c, 0x42, + 0x47, 0xf8, 0xbc, 0xe6, 0xe5, 0x63, 0xa4, 0x40, + 0xf2, 0x77, 0x03, 0x7d, 0x81, 0x2d, 0xeb, 0x33, + 0xa0, 0xf4, 0xa1, 0x39, 0x45, 0xd8, 0x98, 0xc2, + 0x96, 0x4f, 0xe3, 0x42, 0xe2, 0xfe, 0x1a, 0x7f, + 0x9b, 0x8e, 0xe7, 0xeb, 0x4a, 0x7c, 0x0f, 0x9e, + 0x16, 0x2b, 0xce, 0x33, 0x57, 0x6b, 0x31, 0x5e, + 0xce, 0xcb, 0xb6, 0x40, 0x68, 0x37, 0xbf, 0x51, + 0xf5, +]; + +#[rustfmt::skip] +const COMPRESSED_G: [u8; 33] = [ + 0x03, 0x6b, 0x17, 0xd1, 0xf2, 0xe1, 0x2c, 0x42, + 0x47, 0xf8, 0xbc, 0xe6, 0xe5, 0x63, 0xa4, 0x40, + 0xf2, 0x77, 0x03, 0x7d, 0x81, 0x2d, 0xeb, 0x33, + 0xa0, 0xf4, 0xa1, 0x39, 0x45, 0xd8, 0x98, 0xc2, + 0x96, +]; + +#[rustfmt::skip] +const COMPRESSED_3G: [u8; 33] = [ + 0x02, 0x5e, 0xcb, 0xe4, 0xd1, 0xa6, 0x33, 0x0a, + 0x44, 0xc8, 0xf7, 0xef, 0x95, 0x1d, 0x4b, 0xf1, + 0x65, 0xe6, 0xc6, 0xb7, 0x21, 0xef, 0xad, 0xa9, + 0x85, 0xfb, 0x41, 0x66, 0x1b, 0xc6, 0xe7, 0xfd, + 0x6c, +]; + +fn fe(limbs: [u64; 4]) -> P256FieldElement { + P256FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_encode_uncompressed() { + assert_eq!(encode_uncompressed(&fe(G_X), &fe(G_Y)), UNCOMPRESSED_G); +} + +#[test] +fn known_answer_encode_compressed() { + assert_eq!(encode_compressed(&fe(G_X), &fe(G_Y)), COMPRESSED_G, "G has odd y -> tag 0x03"); + assert_eq!(encode_compressed(&fe(P3_X), &fe(P3_Y)), COMPRESSED_3G, "3G has even y -> tag 0x02"); +} + +#[test] +fn known_answer_decode_uncompressed() { + let (x, y) = decode(&UNCOMPRESSED_G).expect("valid uncompressed point"); + assert_eq!(x, fe(G_X)); + assert_eq!(y, fe(G_Y)); +} + +#[test] +fn known_answer_decode_compressed() { + let (x, y) = decode(&COMPRESSED_G).expect("valid compressed point, tag 0x03"); + assert_eq!(x, fe(G_X)); + assert_eq!(y, fe(G_Y)); + + let (x3, y3) = decode(&COMPRESSED_3G).expect("valid compressed point, tag 0x02"); + assert_eq!(x3, fe(P3_X)); + assert_eq!(y3, fe(P3_Y)); +} + +#[test] +fn round_trip_uncompressed_and_compressed() { + for (x, y) in [(G_X, G_Y), (P3_X, P3_Y)] { + let uncompressed = encode_uncompressed(&fe(x), &fe(y)); + assert_eq!(decode(&uncompressed), Some((fe(x), fe(y)))); + + let compressed = encode_compressed(&fe(x), &fe(y)); + assert_eq!(decode(&compressed), Some((fe(x), fe(y)))); + + assert!(decode_point(&uncompressed).unwrap().to_affine().is_some()); + } +} + +#[test] +fn decode_rejects_malformed_input() { + // wrong length + assert_eq!(decode(&[]), None); + assert_eq!(decode(&[0x04; 64]), None); + assert_eq!(decode(&[0x04; 66]), None); + assert_eq!(decode(&[0x02; 32]), None); + + // wrong tag for the given length + let mut bad_tag_65 = UNCOMPRESSED_G; + bad_tag_65[0] = 0x02; + assert_eq!(decode(&bad_tag_65), None); + + let mut bad_tag_33 = COMPRESSED_G; + bad_tag_33[0] = 0x04; + assert_eq!(decode(&bad_tag_33), None); + + let mut bad_tag_33_other = COMPRESSED_G; + bad_tag_33_other[0] = 0x01; + assert_eq!(decode(&bad_tag_33_other), None); + + // the single-byte point-at-infinity encoding is not accepted (see module docs) + assert_eq!(decode(&[0x00]), None); + + // x out of range [0, p) for the compressed case (all-0xff x, > p) + let mut x_too_big = COMPRESSED_G; + x_too_big[1..33].fill(0xff); + assert_eq!(decode(&x_too_big), None); + + // an x with no square root (curve equation has no solution) must be rejected + let mut no_root = COMPRESSED_G; + no_root[1..33].fill(0); + no_root[32] = 2; // x = 2, confirmed (in the Python derivation) to have no square root + assert_eq!(decode(&no_root), None); + + // uncompressed point not on the curve (G's x with a tampered y) + let mut not_on_curve = UNCOMPRESSED_G; + not_on_curve[64] ^= 0x01; + assert_eq!(decode(¬_on_curve), None); + + // y out of range [0, p) for the uncompressed case + let mut y_too_big = UNCOMPRESSED_G; + y_too_big[33..65].fill(0xff); + assert_eq!(decode(&y_too_big), None); +} diff --git a/crypto/ec/tests/p256_wnaf_tests.rs b/crypto/ec/tests/p256_wnaf_tests.rs new file mode 100644 index 000000000..35b6e44c0 --- /dev/null +++ b/crypto/ec/tests/p256_wnaf_tests.rs @@ -0,0 +1,178 @@ +//! Known-answer tests for [`shamir_multiply`] (`[u]G + [v]Q`). +//! +//! `Q` below is `d*G` for a fixed test-only `d` (not a real key). Expected results were computed +//! independently in Python from the SP 800-186 Appendix A.1.1 affine group law, the same way as +//! `p256_comb_tests.rs`, with `random.seed(808)`. + +use bouncycastle_ec::p256::P256FieldElement; +use bouncycastle_ec::p256_comb::comb_multiply_base_point; +use bouncycastle_ec::p256_point::P256JacobianPoint; +use bouncycastle_ec::p256_scalar::{P256PublicScalar, P256Scalar}; +use bouncycastle_ec::p256_wnaf::shamir_multiply; + +const Q_X: [u64; 4] = + [0xb8ead9e9b37489ae, 0xb2bd6640d485e210, 0x65021b418d419835, 0x8db3ea9c500e21de]; +const Q_Y: [u64; 4] = + [0xdfda9d4b2257b9f5, 0xaa237e07ab5789f0, 0x66dc9154eb749075, 0x1a8b7e4595d73287]; + +fn q() -> P256JacobianPoint { + P256JacobianPoint::from_affine( + P256FieldElement::from_limbs(Q_X), + P256FieldElement::from_limbs(Q_Y), + ) +} + +fn scalar(limbs: [u64; 4]) -> P256PublicScalar { + P256PublicScalar::from_limbs(limbs) +} + +fn assert_result_is(u: [u64; 4], v: [u64; 4], expected_x: [u64; 4], expected_y: [u64; 4]) { + let result = shamir_multiply(&scalar(u), &scalar(v), &q()); + let (rx, ry) = result.to_affine().expect("expected a non-infinity result"); + assert_eq!(rx, P256FieldElement::from_limbs(expected_x), "x mismatch for u={u:x?} v={v:x?}"); + assert_eq!(ry, P256FieldElement::from_limbs(expected_y), "y mismatch for u={u:x?} v={v:x?}"); +} + +#[test] +fn known_answer_small_u_v() { + assert_result_is( + [12345, 0, 0, 0], + [6789, 0, 0, 0], + [0x891ec66a07d3dd0b, 0xd34e6b109a97879d, 0xd3e6c8c8eb8db51a, 0x3d5afe5709f19122], + [0x84ed8e7ab7daaaf3, 0x94af16193179d563, 0x7e455089f00cecea, 0x2948dbc14abcc92a], + ); + assert_result_is( + [1, 0, 0, 0], + [1, 0, 0, 0], + [0x7993b285982367e5, 0x1380b2a785d18e95, 0x73b3a283cdd79a9b, 0xf351345def9ede77], + [0x4ca5032233e7894e, 0x620febfc3aba9618, 0xdab82d8b820e5b60, 0x4f718d4f426025f4], + ); +} + +#[test] +fn known_answer_large_u_v() { + assert_result_is( + [0xc2c21a69f006dcc2, 0xe0c8cc433ca149bd, 0x6346ab1c59d237b2, 0xc71fe0537ca93045], + [0xd30b99fb3eb640b9, 0xa0f42e6188c6f196, 0x2ed48753c852873b, 0x44a1f0a402f4eea6], + [0x0b74e63145c21821, 0x78079cd518bbff20, 0x0d370eced42e7464, 0x23b0e4ecaea7f530], + [0x56b583922e2e277e, 0x35cf03e7b50a1629, 0x48c996a406feac77, 0x27d677f7140d39eb], + ); + assert_result_is( + [0xb3741c87117f9b32, 0x5a3b774dc31e0a4f, 0x9468f6370a9c3f95, 0x9838c9b21b004bea], + [0xec09a621f2563f9e, 0x853761ae35c74b86, 0xede6b87945b9a46d, 0xa01d685690b61514], + [0x5c2ae8035fedbd6b, 0x783ec0ae4f9b4185, 0xd94028e563cb656f, 0xdf974dc081216a1b], + [0x94f029efe09a9a39, 0xd9f5e6e81615ddd6, 0x8fe9ac9c96b26409, 0xe90324b8f810ba2e], + ); +} + +#[test] +fn edge_case_v_is_zero_reduces_to_u_times_g() { + // u=5, v=0 must equal 5*G, i.e. p256_comb_tests's known 5*G value (P5 there). + assert_result_is( + [5, 0, 0, 0], + [0, 0, 0, 0], + [0x21554a0dc3d033ed, 0xef8c82fd1f5be524, 0xd784c85608668fdf, 0x51590b7a515140d2], + [0xd1d0bb44fda16da4, 0x0d012f00d4d80888, 0x8ae1bf36bf8a7926, 0xe0c17da8904a727d], + ); +} + +#[test] +fn edge_case_u_is_zero_reduces_to_v_times_q() { + assert_result_is( + [0, 0, 0, 0], + [5, 0, 0, 0], + [0x2f5f2aad64286296, 0x1d6281be4abe14c0, 0x88b12452a34a8a4a, 0x100da2f784e9e086], + [0xf287dc33778a55c1, 0x0b35c3de7f7cadbc, 0x347663651c0a8ae8, 0x2941b7ace017645b], + ); +} + +#[test] +fn edge_case_both_zero_is_infinity() { + let result = shamir_multiply(&scalar([0, 0, 0, 0]), &scalar([0, 0, 0, 0]), &q()); + assert!(result.to_affine().is_none()); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn cross_checked_against_the_comb_multiplier_over_many_pseudorandom_scalars() { + // shamir_multiply(k, 0, anything) must equal comb_multiply_base_point(k): two independently + // implemented multipliers (fixed-base comb vs. interleaved wNAF) computing the same [k]G. + let mut rng = Xorshift64(0xD1B54A32D192ED03); + for _ in 0..500 { + let k_limbs = rng.next_limbs(); + let via_comb = comb_multiply_base_point(&P256Scalar::from_limbs(k_limbs)); + let via_wnaf = + shamir_multiply(&P256PublicScalar::from_limbs(k_limbs), &scalar([0, 0, 0, 0]), &q()); + assert_eq!(via_comb.to_affine(), via_wnaf.to_affine(), "k = {k_limbs:x?}"); + } +} + +#[test] +fn edge_case_u_v_equal_n_minus_1() { + let n_minus_1: [u64; 4] = + [0xf3b9cac2fc632550, 0xbce6faada7179e84, 0xffffffffffffffff, 0xffffffff00000000]; + assert_result_is( + n_minus_1, + n_minus_1, + [0x7993b285982367e5, 0x1380b2a785d18e95, 0x73b3a283cdd79a9b, 0xf351345def9ede77], + [0xb35afcddcc1876b1, 0x9df01404c54569e7, 0x2547d2747df1a49f, 0xb08e72afbd9fda0c], + ); +} + +/// Every other test in this file uses one fixed `Q = dG`. The three `Q` values here each take a +/// path that one cannot: `Q = infinity` makes every `[v]Q` digit an addition of the identity, +/// `Q = G` makes the two precomputed odd-multiple tables identical (so the interleaved +/// additions hit the same-point case of `add_vartime`), and `Q = -G` makes them negatives of +/// each other (the opposite-point case, and with `u == v` a final result of infinity). Expected +/// values come from the fixed-base comb multiplier on the combined scalar, which is pinned by +/// its own known-answer tests. +#[test] +fn edge_case_q_is_infinity_g_or_minus_g() { + let g = P256JacobianPoint::from_affine( + P256FieldElement::from_limbs(bouncycastle_ec::p256_domain::G_X_LIMBS), + P256FieldElement::from_limbs(bouncycastle_ec::p256_domain::G_Y_LIMBS), + ); + let mut u = [0u64; 4]; + u[0] = 12345; + let mut v = [0u64; 4]; + v[0] = 6789; + let mut u_plus_v = [0u64; 4]; + u_plus_v[0] = 19134; + let mut u_minus_v = [0u64; 4]; + u_minus_v[0] = 5556; + let via_comb = |k: [u64; 4]| comb_multiply_base_point(&P256Scalar::from_limbs(k)).to_affine(); + + // [u]G + [v]infinity == [u]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &P256JacobianPoint::INFINITY).to_affine(), + via_comb(u) + ); + // [u]G + [v]G == [u+v]G + assert_eq!(shamir_multiply(&scalar(u), &scalar(v), &g).to_affine(), via_comb(u_plus_v)); + // [u]G + [v](-G) == [u-v]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &g.negate()).to_affine(), + via_comb(u_minus_v) + ); + // [u]G + [u](-G) == infinity + assert!(shamir_multiply(&scalar(u), &scalar(u), &g.negate()).is_infinity().to_bool()); + // [0]G + [v]G == [v]G: the G table is never consulted, only Q's + assert_eq!(shamir_multiply(&scalar([0u64; 4]), &scalar(v), &g).to_affine(), via_comb(v)); +} diff --git a/crypto/ec/tests/p256k1_comb_tests.rs b/crypto/ec/tests/p256k1_comb_tests.rs new file mode 100644 index 000000000..56582eed6 --- /dev/null +++ b/crypto/ec/tests/p256k1_comb_tests.rs @@ -0,0 +1,70 @@ +//! Known-answer tests for [`comb_multiply_base_point`] (`[k]G`), secp256k1. +//! +//! `k = 2, 3` reuse the values verified independently in `p256k1_point_tests.rs`; `k` for +//! `known_answer_large_k` and `n-1` (where `[k]G = -G`) were computed the same way (Python affine +//! group law, `a = 0`), `random.seed(1246)` for the former. + +use bouncycastle_ec::p256k1::P256K1FieldElement; +use bouncycastle_ec::p256k1_comb::comb_multiply_base_point; +use bouncycastle_ec::p256k1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p256k1_scalar::P256K1Scalar; + +fn scalar(limbs: [u64; 4]) -> P256K1Scalar { + P256K1Scalar::from_limbs(limbs) +} + +fn assert_result_is(k_limbs: [u64; 4], x: [u64; 4], y: [u64; 4]) { + let result = comb_multiply_base_point(&scalar(k_limbs)); + let (rx, ry) = result.to_affine().expect("[k]G must not be infinity for k in (0, n)"); + assert_eq!(rx, P256K1FieldElement::from_limbs(x), "x mismatch for k = {k_limbs:x?}"); + assert_eq!(ry, P256K1FieldElement::from_limbs(y), "y mismatch for k = {k_limbs:x?}"); +} + +#[test] +fn known_answer_small_k() { + assert_result_is([1, 0, 0, 0], G_X_LIMBS, G_Y_LIMBS); + assert_result_is( + [2, 0, 0, 0], + [0xabac09b95c709ee5, 0x5c778e4b8cef3ca7, 0x3045406e95c07cd8, 0xc6047f9441ed7d6d], + [0x236431a950cfe52a, 0xf7f632653266d0e1, 0xa3c58419466ceaee, 0x1ae168fea63dc339], + ); + assert_result_is( + [3, 0, 0, 0], + [0x8601f113bce036f9, 0xb531c845836f99b0, 0x49344f85f89d5229, 0xf9308a019258c310], + [0x6cb9fd7584b8e672, 0x6500a99934c2231b, 0x0fe337e62a37f356, 0x388f7b0f632de814], + ); +} + +#[test] +fn known_answer_large_k() { + let k: [u64; 4] = + [0x0f6492cb2a9bcd60, 0x203c90bb86fdcf60, 0x973e8813bb3b3b8f, 0x0a5b2b964ff75db2]; + assert_result_is( + k, + [0xa08ae34de00c7157, 0xe3005a4271afb456, 0x76db92696a558cea, 0xef077c553000618c], + [0xcca36d4110e18f30, 0xf02ed559a3150cb0, 0xa3a0f3cf2da0aef0, 0x53a38aae0db4d23d], + ); +} + +#[test] +fn edge_case_k_equals_n_minus_1() { + // (n-1)*G == -G: same x as G, negated y. + let n_minus_1: [u64; 4] = + [0xbfd25e8cd0364140, 0xbaaedce6af48a03b, 0xfffffffffffffffe, 0xffffffffffffffff]; + assert_result_is( + n_minus_1, + G_X_LIMBS, + [0x63b82f6f04ef2777, 0x02e84bb7597aabe6, 0xa25b0403f1eef757, 0xb7c52588d95c3b9a], + ); +} + +/// `k = 0` is the one scalar for which `[k]G` is the identity: every comb digit is `0`, so every +/// round selects table entry `0` (the infinity sentinel) and the accumulator never leaves +/// `INFINITY`. Nothing in the signing path can produce it (`k` is in `[1, n-1]` by construction), +/// which is exactly why it needs pinning here rather than being left to chance. +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let result = comb_multiply_base_point(&scalar([0u64; 4])); + assert!(result.is_infinity().to_bool()); + assert!(result.to_affine().is_none()); +} diff --git a/crypto/ec/tests/p256k1_extra_bits_tests.rs b/crypto/ec/tests/p256k1_extra_bits_tests.rs new file mode 100644 index 000000000..ca08844c9 --- /dev/null +++ b/crypto/ec/tests/p256k1_extra_bits_tests.rs @@ -0,0 +1,153 @@ +//! Known-answer tests for [`reduce_wide_bits_mod_n_minus_1`] (FIPS 186-5 Appendix A.4.1 steps +//! 3-5): `x = bits2int(X)`, `x = x mod (n-1)`, `x = x + 1`. Expected values computed independently +//! in Python via `(int.from_bytes(bytes, 'big') % (n - 1)) + 1` with `n` from SEC 2 v2 §2.4.1, +//! not derived from this crate's own reduction code -- see `extra_bits_tests.rs`'s docs for why +//! this independent-reference pinning matters. Inputs are the 40 bytes (320 bits) `keys_p256k1` +//! and `ecdsa_p256k1` actually draw, except where a shorter input is the point. + +use bouncycastle_ec::p256k1_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::p256k1_scalar::N_LIMBS; +use bouncycastle_ec::p256k1_sec1::be_bytes_from_limbs; +use bouncycastle_hex::decode as hex_decode; + +fn bytes32(hex: &str) -> [u8; 32] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +#[test] +fn known_answer_repeating_pattern() { + let input: Vec = [0x01u8, 0x02, 0x03, 0x04].iter().cloned().cycle().take(40).collect(); + let expected = bytes32("01020304010203040249e29e6a7b13c455f8adf82d7885809a9646b25ae53e05"); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_ff() { + let input = [0xffu8; 40]; + let expected = bytes32("00000000000000014551231950b75fc4402da1732fc9bec00000000000000000"); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_zero_yields_one() { + // bits2int(0) = 0; 0 mod (n-1) = 0; +1 = 1 -- the smallest value Appendix A.4.1 can produce. + let input = [0u8; 40]; + let mut expected = [0u8; 32]; + expected[31] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +/// An `n`-width input below `n - 1` is its own residue, so the result is `x + 1`: the case the +/// single-subtraction reduction this module replaced was built for, kept as a check that the +/// wide reduction agrees with it on its home ground. `x` is pseudorandom, from Python. +#[test] +fn known_answer_n_width_input_below_n_minus_1() { + let x = bytes32("72d50095457ed5710b049ab71dbafb06b0525afa51a26b8a310022084564664a"); + let mut x_plus_1 = x; + x_plus_1[31] += 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&x).to_be_bytes(), x_plus_1); +} + +/// The boundaries of Appendix A.4.1's own arithmetic: `n - 1` is the modulus of step 4, so it +/// reduces to `0` and step 5 makes it `1`; `n - 2` is the largest residue, so it comes out as +/// `n - 1`, the top of the output interval; and `n` itself is `1 mod (n-1)`, so `2`. +#[test] +fn known_answer_at_the_n_minus_1_boundary() { + let n = be_bytes_from_limbs(&N_LIMBS); + let mut n_minus_1 = n; + n_minus_1[31] -= 1; // n is odd, so neither subtraction borrows + let mut n_minus_2 = n; + n_minus_2[31] -= 2; + + let mut one = [0u8; 32]; + one[31] = 1; + let mut two = [0u8; 32]; + two[31] = 2; + + let mut input = [0u8; 40]; + input[8..].copy_from_slice(&n_minus_1); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), one, "n - 1 -> 1"); + input[8..].copy_from_slice(&n_minus_2); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), n_minus_1, "n - 2 -> n - 1"); + input[8..].copy_from_slice(&n); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), two, "n -> 2"); +} + +/// The bit-serial algorithm this module implemented before switching to Barrett reduction, kept +/// here as an independent reference: Horner's rule over the input bits, doubling the running +/// remainder and subtracting `n - 1` when it is exceeded, with the carry out of the top limb folded +/// back in as `2^256 mod (n-1) = 2^256 - (n-1)`. Slow, obviously correct, and sharing no +/// code with the Barrett path beyond `nat`'s add and sub. +fn reference_bit_serial(bytes: &[u8]) -> [u8; 32] { + use bouncycastle_ec::nat; + let n = bouncycastle_ec::p256k1_scalar::N_LIMBS; + let mut m = n; + m[0] -= 1; // n is odd + let two_pow_mod_m = nat::sub(&[0u64; 4], &m).0; // 2^256 - m, wrapping + let mut acc = [0u64; 4]; + for &byte in bytes { + for bit_idx in (0..8).rev() { + let (mut doubled, carry) = nat::add(&acc, &acc); + doubled[0] |= ((byte >> bit_idx) & 1) as u64; + if carry == 1 { + doubled = nat::add(&doubled, &two_pow_mod_m).0; + } + let (reduced, borrow) = nat::sub(&doubled, &m); + acc = if borrow == 1 { doubled } else { reduced }; + } + } + let plus_one = nat::add(&acc, &[1u64, 0, 0, 0]).0; + bouncycastle_ec::p256k1_sec1::be_bytes_from_limbs(&plus_one) +} + +/// xorshift64*, fixed seed, as in the `ec` crate's property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } +} + +/// Barrett against the bit-serial reference over pseudorandom inputs at every width from one byte +/// up to the 63-byte maximum the function accepts, biased towards the widths actually drawn +/// (40 bytes) and towards values with long runs of set bits, where Barrett's quotient estimate is +/// furthest off and the most conditional subtractions are needed. +#[test] +fn agrees_with_the_bit_serial_reference_over_many_pseudorandom_inputs() { + let mut rng = Xorshift64(0xBA77E770_0000_0004); + for i in 0..3000 { + let len = match i % 4 { + 0 => 40, + 1 => 63, + _ => 1 + (rng.next_u64() as usize % 63), + }; + let mut bytes = vec![0u8; len]; + for chunk in bytes.chunks_mut(8) { + let word = rng.next_u64().to_be_bytes(); + chunk.copy_from_slice(&word[..chunk.len()]); + } + if i % 3 == 0 { + for b in bytes.iter_mut().take(len / 2) { + *b = 0xff; + } + } + assert_eq!( + reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), + reference_bit_serial(&bytes), + "len = {len}, bytes = {bytes:02x?}" + ); + } +} + +#[test] +fn maximum_width_all_ones_input() { + // 2^504 - 1, the largest input accepted: Barrett's precondition holds with room to spare. + let bytes = [0xffu8; 63]; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), reference_bit_serial(&bytes)); +} diff --git a/crypto/ec/tests/p256k1_field_tests.rs b/crypto/ec/tests/p256k1_field_tests.rs new file mode 100644 index 000000000..b4b6db426 --- /dev/null +++ b/crypto/ec/tests/p256k1_field_tests.rs @@ -0,0 +1,369 @@ +//! Known-answer tests for [`P256K1FieldElement`] arithmetic. +//! +//! `p = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1` (SEC 2 v2 §2.4.1). Expected values +//! computed independently in Python: +//! +//! ```text +//! p = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1 +//! random.seed(256257) +//! vals = [random.randrange(1, p) for _ in range(4)] + [0] +//! # for each pair (i, j) with i < j: (vals[i]+vals[j]) % p, (vals[i]-vals[j]) % p, (vals[i]*vals[j]) % p +//! # for each i: (-vals[i]) % p, pow(vals[i], p-2, p) if vals[i] != 0 else 0 +//! ``` + +use bouncycastle_ec::p256k1::P256K1FieldElement; + +const VALS_0: [u64; 4] = + [0x1d79dcf976de30b9, 0xc397f473860488a8, 0x71ee41064b94ee22, 0x250bcbb11b3bf238]; +const VALS_1: [u64; 4] = + [0x54f75f31db45259c, 0x6e4dc945e3e2cd85, 0x8931d88ee2f18ae1, 0xe157765b6787ae5a]; +const VALS_2: [u64; 4] = + [0xb5fc00c62432f002, 0x47d7278753fc4fd0, 0x177512e4169a1e3f, 0x694e99e39f271c59]; +const VALS_3: [u64; 4] = + [0x363bc43c067e7409, 0xc631034a05e67b84, 0x106a3de955343c4e, 0x2ee9135e80c0bdf8]; +const VALS_4: [u64; 4] = [0, 0, 0, 0]; + +const ADD_0_1: [u64; 4] = + [0x72713c2c52235a26, 0x31e5bdb969e7562d, 0xfb2019952e867904, 0x0663420c82c3a092]; +const SUB_0_1: [u64; 4] = + [0xc8827dc69b99074c, 0x554a2b2da221bb22, 0xe8bc687768a36341, 0x43b45555b3b443dd]; +const MUL_0_1: [u64; 4] = + [0x12b3df9da2d0f757, 0xba04254ab048cdaa, 0xc342d06ed5e2a7b8, 0x597190cde3062a98]; +const ADD_0_2: [u64; 4] = + [0xd375ddbf9b1120bb, 0x0b6f1bfada00d878, 0x896353ea622f0c62, 0x8e5a6594ba630e91]; +const SUB_0_2: [u64; 4] = + [0x677ddc3252ab3ce6, 0x7bc0ccec320838d7, 0x5a792e2234facfe3, 0xbbbd31cd7c14d5df]; +const MUL_0_2: [u64; 4] = + [0xac073780ea57fff0, 0x2a6a4a884c941e9c, 0x3c08757bb49d5c72, 0xe297b3604b6e4881]; +const ADD_0_3: [u64; 4] = + [0x53b5a1357d5ca4c2, 0x89c8f7bd8beb042c, 0x82587eefa0c92a71, 0x53f4df0f9bfcb030]; +const SUB_0_3: [u64; 4] = + [0xe73e18bc705fb8df, 0xfd66f129801e0d23, 0x6184031cf660b1d3, 0xf622b8529a7b3440]; +const MUL_0_3: [u64; 4] = + [0xfc5beeeb54d24c73, 0x779717b847d0819a, 0x2448529800ab9845, 0xa491eebb026bfed1]; +const ADD_1_2: [u64; 4] = + [0x0af35ff8ff78196f, 0xb624f0cd37df1d56, 0xa0a6eb72f98ba920, 0x4aa6103f06aecab3]; +const SUB_1_2: [u64; 4] = + [0x9efb5e6bb712359a, 0x2676a1be8fe67db4, 0x71bcc5aacc576ca2, 0x7808dc77c8609201]; +const MUL_1_2: [u64; 4] = + [0x59416a30b93e6b87, 0x405aef00f1c086d3, 0x72995268df93a337, 0xce46213e77403999]; +const ADD_1_3: [u64; 4] = + [0x8b33236ee1c39d76, 0x347ecc8fe9c94909, 0x999c16783825c730, 0x104089b9e8486c52]; +const SUB_1_3: [u64; 4] = + [0x1ebb9af5d4c6b193, 0xa81cc5fbddfc5201, 0x78c79aa58dbd4e92, 0xb26e62fce6c6f062]; +const MUL_1_3: [u64; 4] = + [0x7a070b0eeee214f2, 0x5c7da86c1035cd1c, 0xf83b70133a2410d3, 0x313edd0df41b5d3e]; +const ADD_2_3: [u64; 4] = + [0xec37c5022ab1640b, 0x0e082ad159e2cb54, 0x27df50cd6bce5a8e, 0x9837ad421fe7da51]; +const SUB_2_3: [u64; 4] = + [0x7fc03c8a1db47bf9, 0x81a6243d4e15d44c, 0x070ad4fac165e1f0, 0x3a6586851e665e61]; +const MUL_2_3: [u64; 4] = + [0x54156f3dc5daeeb9, 0x57a6a66eeb4ae815, 0xc028aa05849daab8, 0xef738698d341251a]; + +const NEG_0: [u64; 4] = + [0xe28623058921cb76, 0x3c680b8c79fb7757, 0x8e11bef9b46b11dd, 0xdaf4344ee4c40dc7]; +const INV_0: [u64; 4] = + [0x51fbbe545c1fc928, 0xa473aac9d84c2261, 0xc983cfb518b3a5ae, 0x5c7cef3ed3fb59ef]; +const NEG_1: [u64; 4] = + [0xab08a0cd24bad693, 0x91b236ba1c1d327a, 0x76ce27711d0e751e, 0x1ea889a4987851a5]; +const INV_1: [u64; 4] = + [0xd9224b92d2ad8cec, 0x30f0064648dbd35b, 0xd4f070ef5468f757, 0x53a7541b1355bcdb]; +const NEG_2: [u64; 4] = + [0x4a03ff38dbcd0c2d, 0xb828d878ac03b02f, 0xe88aed1be965e1c0, 0x96b1661c60d8e3a6]; +const INV_2: [u64; 4] = + [0xb0c70ca88a757670, 0xd914ab32dd7a94d3, 0xdc060a4814c4e5d7, 0x1a8c4087423108d7]; +const NEG_3: [u64; 4] = + [0xc9c43bc2f9818826, 0x39cefcb5fa19847b, 0xef95c216aacbc3b1, 0xd116eca17f3f4207]; +const INV_3: [u64; 4] = + [0x07d325f509549337, 0x57e3d49afb2fbb81, 0xce4aabc4f0ecfafe, 0xb07744b63424bcec]; + +fn fe(limbs: [u64; 4]) -> P256K1FieldElement { + P256K1FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3].map(fe); + let pairs: [(usize, usize, [u64; 4], [u64; 4], [u64; 4]); 6] = [ + (0, 1, ADD_0_1, SUB_0_1, MUL_0_1), + (0, 2, ADD_0_2, SUB_0_2, MUL_0_2), + (0, 3, ADD_0_3, SUB_0_3, MUL_0_3), + (1, 2, ADD_1_2, SUB_1_2, MUL_1_2), + (1, 3, ADD_1_3, SUB_1_3, MUL_1_3), + (2, 3, ADD_2_3, SUB_2_3, MUL_2_3), + ]; + for (i, j, a, s, m) in pairs { + assert_eq!(vals[i].add(&vals[j]), fe(a), "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), fe(a), "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), fe(s), "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), fe(m), "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), fe(m), "mul is commutative ({j},{i})"); + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3]; + let negs = [NEG_0, NEG_1, NEG_2, NEG_3]; + let invs = [INV_0, INV_1, INV_2, INV_3]; + for i in 0..4 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4].map(fe); + for &v in &vals { + assert_eq!(v.add(&P256K1FieldElement::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&P256K1FieldElement::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), P256K1FieldElement::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), P256K1FieldElement::ZERO, "x + (-x) == 0"); + if v != P256K1FieldElement::ZERO { + assert_eq!(v.mul(&v.invert()), P256K1FieldElement::ONE, "x * x^-1 == 1"); + } else { + assert_eq!(v.invert(), P256K1FieldElement::ZERO, "0^-1 == 0 by convention"); + } + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + assert_eq!(fe(bouncycastle_ec::p256k1::P_LIMBS), P256K1FieldElement::ZERO); + let p_plus_1: [u64; 4] = + [0xfffffffefffffc30, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff]; + assert_eq!(fe(p_plus_1), P256K1FieldElement::ONE); + + let all_ones = [u64::MAX; 4]; + let expected_c_minus_1 = fe([0x00000001000003d0, 0, 0, 0]); + assert_eq!(fe(all_ones), expected_c_minus_1); +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..4 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4]; + for i in 0..vals.len() { + for j in (i + 1)..vals.len() { + assert_ne!(fe(vals[i]), fe(vals[j]), "VALS_{i} must differ from VALS_{j}"); + } + } +} + +/// xorshift64* PRNG, fixed seed: same rationale as the P-256 field test's property check. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0x853C49E6748FEA9B); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != P256K1FieldElement::ZERO { + assert_eq!(a.mul(&a.invert()), P256K1FieldElement::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE0000000002); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 4], + { + let mut l = [0u64; 4]; + l[0] = 1; + l + }, + [u64::MAX; 4], + { + let mut l = [u64::MAX; 4]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `is_zero` is the mask every exceptional-case select in this crate's point arithmetic keys +/// off (infinity detection, the `H == 0` / `R == 0` same-point and opposite-point cases), and +/// otherwise exercised only through those selects. Both truth values are pinned here, with +/// the nonzero side walked across every limb position so a mask that inspected only some limbs +/// would be caught. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(P256K1FieldElement::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::p256k1::P_LIMBS).is_zero().to_bool(), "p reduces to 0"); + assert!(!P256K1FieldElement::ONE.is_zero().to_bool()); + for limb_idx in 0..4 { + let mut limbs = [0u64; 4]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `add`'s carry-out correction and `sub`'s borrow correction, pinned at the operands that force +/// them: `(p-1) + (p-1)` is the largest sum two canonical elements can form, and `0 - 1` the +/// smallest difference. The expected values are `p - 2` and `p - 1` themselves (computed from +/// `p` in Python), so this is a check of the wrap-around handling, not of the digits of `p`. +#[test] +fn known_answer_add_carry_and_sub_borrow_at_the_extremes() { + let p_minus_1 = + fe([0xfffffffefffffc2e, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff]); + let p_minus_2 = + fe([0xfffffffefffffc2d, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff]); + let two = fe([0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]); + + assert_eq!(p_minus_1.add(&p_minus_1), p_minus_2, "(p-1) + (p-1) == p-2"); + assert_eq!(p_minus_1.add(&P256K1FieldElement::ONE), P256K1FieldElement::ZERO, "(p-1) + 1 == 0"); + assert_eq!(P256K1FieldElement::ZERO.sub(&P256K1FieldElement::ONE), p_minus_1, "0 - 1 == p-1"); + assert_eq!(P256K1FieldElement::ONE.sub(&p_minus_1), two, "1 - (p-1) == 2"); + assert_eq!(p_minus_1.negate(), P256K1FieldElement::ONE, "-(p-1) == 1"); + assert_eq!(P256K1FieldElement::ONE.invert(), P256K1FieldElement::ONE, "1^-1 == 1"); + assert_eq!(p_minus_1.invert(), p_minus_1, "(p-1)^-1 == p-1, since (p-1)^2 == 1"); +} + +/// Products at the reduction's extremes rather than in the middle of its range, the same class +/// of check `p256_field_tests.rs`/`p384_field_tests.rs`/`sm2_field_tests.rs` already carry and +/// this curve did not: `(p-1)^2` and `(p-1)(p-2)` (the largest products two canonical elements +/// can form), operands within `2^64` of `p`, and operands with the top bit set. Expected values +/// are Python's `(a * b) % p`, computed independently of this crate's arithmetic +/// (`random.seed(20260918)` for the pseudorandom operands). +#[test] +fn known_answer_mul_at_the_reduction_bounds() { + let cases: [([u64; 4], [u64; 4], [u64; 4]); 7] = [ + // (p-1)^2 == 1 + ( + [0xfffffffefffffc2e, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff], + [0xfffffffefffffc2e, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff], + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + ), + // (p-1)(p-2) == 2 + ( + [0xfffffffefffffc2e, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff], + [0xfffffffefffffc2d, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff], + [0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + ), + // (p-1) * 2^(bits-1) == p - 2^(bits-1) + ( + [0xfffffffefffffc2e, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff], + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x8000000000000000], + [0xfffffffefffffc2f, 0xffffffffffffffff, 0xffffffffffffffff, 0x7fffffffffffffff], + ), + // both operands within 2^64 of p + ( + [0x602e662b710ddd97, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff], + [0xd50f78c27657bf0f, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff], + [0xe3915864fb4e0b00, 0x1ace8c2a3ef88d00, 0x0000000000000000, 0x0000000000000000], + ), + // both operands within 2^64 of p + ( + [0xfbce55e93e856489, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff], + [0xf0fc20c2dcd7c584, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff], + [0x77ae56237cd04fe2, 0x003ef93483f7b6de, 0x0000000000000000, 0x0000000000000000], + ), + // both operands with the top bit set + ( + [0xdfa7b2ef07145e3e, 0xc0a3b3952a340c8f, 0xb8032ba581982589, 0x83ecd0900edcab66], + [0x1cfe03616a76bac0, 0xe47884359ee58544, 0xd9279db7cd8bdeac, 0xd007c87d51d030dd], + [0x9bbe80289abe2572, 0x43e7c0d1df4dd384, 0x40ffed7a3158451b, 0x38af19b041b3c9c2], + ), + // both operands with the top bit set + ( + [0xe76634fa4c5de998, 0x3ed37b9872aebec2, 0xb4ca47f81e911e82, 0xe6688cf86f78dd4b], + [0xb5e03e4b503b7594, 0x0a20973673003543, 0x6f155ffa3ec1be68, 0xf95fc497ff1a57ee], + [0x50b1b28cd4621d6d, 0x5de9bfa26a81ee07, 0xdcd792e32a10cd3e, 0xcdac627a36e71fd3], + ), + ]; + for (a, b, expected) in cases { + assert_eq!(fe(a).mul(&fe(b)), fe(expected), "a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(b).mul(&fe(a)), fe(expected), "commuted: a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(a).square(), fe(a).mul(&fe(a)), "square at the same extreme: a = {a:x?}"); + } +} + +/// The one product shape the reduction-bound cases above cannot reach: a product whose two folds +/// leave the accumulator at or above `2^256`, so that `reduce`'s masked third fold is the step +/// that produces the right answer (`reduce`'s comment in `p256k1.rs` derives the bounds: two folds +/// leave at most one bit above `2^256`, and folding it in cannot carry). None of the cases above +/// reach it -- `(p-1)^2` gives the first fold a high half of `C - 1`, one short of its bound, and +/// its two folds leave exactly `p + 1`, short of `2^256` by `C - 1` -- and neither does random +/// sampling: it needs the low half after the first fold to lie within `hi_1 * C <= 2^66` of +/// `2^256`, probability about `2^-190` per product. The operands are constructed instead, with +/// `a = 2^255` so that `a * b = (b/2) * 2^256` has an all-zero low half and `b/2` chosen so that +/// `(b/2) * C` has every low bit set; computed in Python, independently of this crate's arithmetic: +/// +/// ```text +/// p = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1 +/// C = 2**256 - p +/// hi0 = (2**256 - 1) * pow(C, -1, 2**256) % 2**256 # hi0 * C == 2**256 - 1 (mod 2**256) +/// a, b = 2**255, 2 * hi0 # a * b == hi0 * 2**256, and b < p +/// expected = a * b % p +/// ``` +/// +/// After fold 1 the low half is `2^256 - 1` and the high half `0x3642e7c9`; after fold 2 the +/// value is `2^256 + 0x3642e89815569618`; the third fold adds `C`. A reduction that assumed the +/// bit above `2^256` to be zero after two folds returns `0x3642e89815569618`, short by exactly +/// `C`, and passes every other test in this file. +#[test] +fn known_answer_mul_reaching_the_third_fold() { + let a = [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x8000000000000000]; + let b = [0x4f8fedc45bb5959e, 0x869bb80247b6bf4c, 0xc7727a7ad41a913c, 0x6c85cdf5d558f8cc]; + let expected = [0x3642e899155699e9, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; + let short_by_c = + [0x3642e89815569618, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; + + assert_eq!(fe(a).mul(&fe(b)), fe(expected), "2^255 * b, third fold required"); + assert_eq!(fe(b).mul(&fe(a)), fe(expected), "commuted: b * 2^255, third fold required"); + assert_ne!(fe(a).mul(&fe(b)), fe(short_by_c), "the result a two-fold-only reduction gives"); + assert_eq!(fe(expected).sub(&fe(short_by_c)).to_limbs(), [0x00000001000003d1, 0, 0, 0], "C"); +} diff --git a/crypto/ec/tests/p256k1_mul_tests.rs b/crypto/ec/tests/p256k1_mul_tests.rs new file mode 100644 index 000000000..a95618072 --- /dev/null +++ b/crypto/ec/tests/p256k1_mul_tests.rs @@ -0,0 +1,171 @@ +//! Behavioural tests for [`multiply_point`] (`[k]Q`, constant time in `k`): cross-checked against +//! the fixed-base comb for `Q = G`, against the variable-time wNAF multiplier for a random `Q`, +//! and against `[a k mod n]G` for `Q = [a]G` (three multipliers of independent design computing +//! the same point), plus the edge cases a windowed multiplier can get wrong: `k = 0`, `k = 1`, +//! `k = n-1`, a window of all ones, `Q` the identity, `Q` negated, and `Q` given in a +//! non-canonical-`Z` Jacobian representation. + +use bouncycastle_ec::nat; +use bouncycastle_ec::p256k1::P256K1FieldElement; +use bouncycastle_ec::p256k1_comb::comb_multiply_base_point; +use bouncycastle_ec::p256k1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p256k1_mul::multiply_point; +use bouncycastle_ec::p256k1_point::P256K1JacobianPoint; +use bouncycastle_ec::p256k1_scalar::{ + N_LIMBS, P256K1PublicScalar, P256K1Scalar, P256K1ScalarField, +}; +use bouncycastle_ec::p256k1_wnaf::shamir_multiply; + +const LIMBS: usize = 4; + +fn g() -> P256K1JacobianPoint { + P256K1JacobianPoint::from_affine( + P256K1FieldElement::from_limbs(G_X_LIMBS), + P256K1FieldElement::from_limbs(G_Y_LIMBS), + ) +} + +fn zero() -> [u64; LIMBS] { + [0; LIMBS] +} + +fn small(v: u64) -> [u64; LIMBS] { + let mut limbs = zero(); + limbs[0] = v; + limbs +} + +fn one() -> [u64; LIMBS] { + small(1) +} + +fn n_minus_1() -> [u64; LIMBS] { + let (limbs, borrow) = nat::sub(&N_LIMBS, &one()); + assert_eq!(borrow, 0); + limbs +} + +/// A pseudorandom value reduced into `[0, n)`, so it is a valid scalar for every multiplier. +fn reduced(limbs: [u64; LIMBS]) -> [u64; LIMBS] { + P256K1ScalarField::from_limbs(limbs).to_limbs() +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; LIMBS] { + let mut limbs = zero(); + for limb in limbs.iter_mut() { + *limb = self.next_u64(); + } + limbs + } +} + +#[test] +fn k_times_g_agrees_with_the_comb_multiplier() { + let mut rng = Xorshift64(0x9E3779B97F4A7C15); + let mut cases = vec![one(), small(2), small(3), small(15), small(16), small(17)]; + cases.push(n_minus_1()); + let mut n_minus_2 = n_minus_1(); + n_minus_2[0] -= 1; + cases.push(n_minus_2); + for _ in 0..50 { + cases.push(reduced(rng.next_limbs())); + } + for k in cases { + let via_mul = multiply_point(&P256K1Scalar::from_limbs(k), &g()); + let via_comb = comb_multiply_base_point(&P256K1Scalar::from_limbs(k)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "k = {k:x?}"); + } +} + +#[test] +fn k_times_q_agrees_with_the_wnaf_multiplier_and_with_the_scalar_field() { + let mut rng = Xorshift64(0xC2B2AE3D27D4EB4F); + for _ in 0..50 { + let a = reduced(rng.next_limbs()); + let k = reduced(rng.next_limbs()); + let q = comb_multiply_base_point(&P256K1Scalar::from_limbs(a)); + + let via_mul = multiply_point(&P256K1Scalar::from_limbs(k), &q); + + // [0]G + [k]Q by the variable-time multiplier. + let via_wnaf = shamir_multiply( + &P256K1PublicScalar::from_limbs(zero()), + &P256K1PublicScalar::from_limbs(k), + &q, + ); + assert_eq!(via_mul.to_affine(), via_wnaf.to_affine(), "wnaf: a = {a:x?}, k = {k:x?}"); + + // [a k mod n]G by the fixed-base multiplier. + let ak = P256K1ScalarField::from_limbs(a).mul(&P256K1ScalarField::from_limbs(k)).to_limbs(); + let via_comb = comb_multiply_base_point(&P256K1Scalar::from_limbs(ak)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "comb: a = {a:x?}, k = {k:x?}"); + } +} + +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let q = g().double(); + assert!(multiply_point(&P256K1Scalar::from_limbs(zero()), &q).is_infinity().to_bool()); +} + +#[test] +fn edge_case_k_equals_one_is_q() { + let q = g().double(); + assert_eq!(multiply_point(&P256K1Scalar::from_limbs(one()), &q).to_affine(), q.to_affine()); +} + +#[test] +fn edge_case_k_equals_n_minus_1_is_minus_q() { + let q = g().double().add(&g()); + let result = multiply_point(&P256K1Scalar::from_limbs(n_minus_1()), &q); + assert_eq!(result.to_affine(), q.negate().to_affine()); +} + +#[test] +fn edge_case_q_is_infinity() { + let k = reduced(Xorshift64(0x2545F4914F6CDD1D).next_limbs()); + assert!( + multiply_point(&P256K1Scalar::from_limbs(k), &P256K1JacobianPoint::INFINITY) + .is_infinity() + .to_bool() + ); +} + +#[test] +fn negating_q_negates_the_result() { + let k = reduced(Xorshift64(0x1234_5678_9ABC_DEF1).next_limbs()); + let q = g().double(); + let kq = multiply_point(&P256K1Scalar::from_limbs(k), &q); + let k_neg_q = multiply_point(&P256K1Scalar::from_limbs(k), &q.negate()); + assert_eq!(k_neg_q.to_affine(), kq.negate().to_affine()); +} + +#[test] +fn q_in_a_non_canonical_z_representation_gives_the_same_result() { + // (X, Y, Z) and (λ²X, λ³Y, λZ) are the same affine point; the multiplier must treat them alike. + let k = reduced(Xorshift64(0xDEADBEEFCAFEF00D).next_limbs()); + let q = g().double(); + let lambda = P256K1FieldElement::from_limbs(small(7)); + let lambda2 = lambda.mul(&lambda); + let lambda3 = lambda2.mul(&lambda); + let scaled = + P256K1JacobianPoint { x: q.x.mul(&lambda2), y: q.y.mul(&lambda3), z: q.z.mul(&lambda) }; + assert_eq!(scaled.to_affine(), q.to_affine(), "the scaled point is the same point"); + assert_eq!( + multiply_point(&P256K1Scalar::from_limbs(k), &scaled).to_affine(), + multiply_point(&P256K1Scalar::from_limbs(k), &q).to_affine() + ); +} diff --git a/crypto/ec/tests/p256k1_point_tests.rs b/crypto/ec/tests/p256k1_point_tests.rs new file mode 100644 index 000000000..eaeb54df6 --- /dev/null +++ b/crypto/ec/tests/p256k1_point_tests.rs @@ -0,0 +1,106 @@ +//! Known-answer tests for [`P256K1JacobianPoint`] arithmetic. +//! +//! `G`, `2G`, `3G` and a pseudorandom `kG` were computed independently in Python via the standard +//! affine group law (`a = 0`), not from this crate, and cross-checked against `n*G == infinity`: +//! +//! ```text +//! p = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1 +//! # affine add/double/scalar-mul via the standard a = 0 formulas +//! ``` + +use bouncycastle_ec::p256k1::P256K1FieldElement; +use bouncycastle_ec::p256k1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p256k1_point::P256K1JacobianPoint; + +const TWO_G_X: [u64; 4] = + [0xabac09b95c709ee5, 0x5c778e4b8cef3ca7, 0x3045406e95c07cd8, 0xc6047f9441ed7d6d]; +const TWO_G_Y: [u64; 4] = + [0x236431a950cfe52a, 0xf7f632653266d0e1, 0xa3c58419466ceaee, 0x1ae168fea63dc339]; +const THREE_G_X: [u64; 4] = + [0x8601f113bce036f9, 0xb531c845836f99b0, 0x49344f85f89d5229, 0xf9308a019258c310]; +const THREE_G_Y: [u64; 4] = + [0x6cb9fd7584b8e672, 0x6500a99934c2231b, 0x0fe337e62a37f356, 0x388f7b0f632de814]; + +fn fe(limbs: [u64; 4]) -> P256K1FieldElement { + P256K1FieldElement::from_limbs(limbs) +} + +fn g() -> P256K1JacobianPoint { + P256K1JacobianPoint::from_affine(fe(G_X_LIMBS), fe(G_Y_LIMBS)) +} + +#[test] +fn known_answer_double() { + let doubled = g().double(); + let (x, y) = doubled.to_affine().unwrap(); + assert_eq!(x, fe(TWO_G_X)); + assert_eq!(y, fe(TWO_G_Y)); +} + +#[test] +fn known_answer_add() { + let sum = g().add(&g().double()); + let (x, y) = sum.to_affine().unwrap(); + assert_eq!(x, fe(THREE_G_X)); + assert_eq!(y, fe(THREE_G_Y)); +} + +#[test] +fn point_plus_negation_is_infinity() { + let g = g(); + assert!(g.add(&g.negate()).is_infinity().to_bool()); +} + +#[test] +fn infinity_identities() { + let g = g(); + let infinity = P256K1JacobianPoint::INFINITY; + assert!(infinity.is_infinity().to_bool()); + assert!(infinity.to_affine().is_none()); + + let (x1, y1) = g.add(&infinity).to_affine().unwrap(); + let (x2, y2) = g.to_affine().unwrap(); + assert_eq!((x1, y1), (x2, y2), "P + infinity == P"); + + let (x3, y3) = infinity.add(&g).to_affine().unwrap(); + assert_eq!((x3, y3), (x2, y2), "infinity + P == P"); + + assert!(infinity.add(&infinity).is_infinity().to_bool()); +} + +#[test] +fn scaled_z_representation_still_adds_correctly() { + // (X, Y, Z) and (X*c^2, Y*c^3, Z*c) represent the same affine point for any nonzero c; the + // branch-free add/double formulas must agree regardless of which representative is used. + let c = fe([7, 0, 0, 0]); + let c2 = c.mul(&c); + let c3 = c2.mul(&c); + let g = g(); + let scaled = P256K1JacobianPoint { x: g.x.mul(&c2), y: g.y.mul(&c3), z: g.z.mul(&c) }; + + let canonical_double = g.double().to_affine().unwrap(); + let scaled_double = scaled.double().to_affine().unwrap(); + assert_eq!(canonical_double, scaled_double); + + let canonical_sum = g.add(&g.double()).to_affine().unwrap(); + let scaled_sum = scaled.add(&g.double()).to_affine().unwrap(); + assert_eq!(canonical_sum, scaled_sum); +} + +/// `is_infinity` was previously only ever asserted true. The false side is pinned on the base +/// point and on values derived from it, and doubling the identity must stay at the identity +/// (`double` is total, per its docs, and the comb multiplier's accumulator starts there). +#[test] +fn is_infinity_is_false_for_finite_points_and_infinity_doubles_to_itself() { + let g = g(); + assert!(!g.is_infinity().to_bool(), "G is not the identity"); + assert!(!g.double().is_infinity().to_bool(), "2G is not the identity"); + assert!(!g.negate().is_infinity().to_bool(), "-G is not the identity"); + assert!(!g.add(&g.double()).is_infinity().to_bool(), "3G is not the identity"); + + let inf = P256K1JacobianPoint::INFINITY; + assert!(inf.double().is_infinity().to_bool(), "2 * infinity == infinity"); + assert!(inf.double().to_affine().is_none()); + assert!(inf.negate().is_infinity().to_bool(), "-infinity == infinity"); + assert!(inf.double().add(&g).to_affine().is_some(), "2 * infinity + G == G, a finite point"); +} diff --git a/crypto/ec/tests/p256k1_scalar_tests.rs b/crypto/ec/tests/p256k1_scalar_tests.rs new file mode 100644 index 000000000..1648ca6c4 --- /dev/null +++ b/crypto/ec/tests/p256k1_scalar_tests.rs @@ -0,0 +1,339 @@ +//! Known-answer tests for [`P256K1ScalarField`]/[`P256K1Scalar`]/[`P256K1PublicScalar`] arithmetic +//! mod `n`. Expected values computed independently in Python: +//! +//! ```text +//! n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 +//! random.seed(9384) +//! vals = [random.randrange(1, n) for _ in range(3)] +//! # for each pair (i, j) with i < j: (vals[i]+vals[j]) % n, (vals[i]-vals[j]) % n, (vals[i]*vals[j]) % n +//! # for each i: (-vals[i]) % n, pow(vals[i], n-2, n) +//! ``` + +use bouncycastle_ec::p256k1_scalar::{ + N_LIMBS, P256K1PublicScalar, P256K1Scalar, P256K1ScalarField, +}; + +const VALS_0: [u64; 4] = + [0xc77cb04de57e1b92, 0xb62c49f8ac3809de, 0x87c7a84903ab1b99, 0x68a2bfa3a261cea1]; +const VALS_1: [u64; 4] = + [0x83e1fdb85f5da003, 0x9ebadd01adfd8d5f, 0x87f663fdd6bd5cd4, 0x2909e1888cbdf5e2]; +const VALS_2: [u64; 4] = + [0xbc34c1e9d91e04d7, 0x992805652379593a, 0x2ef83809b8317ce8, 0x26afaaf741ce6162]; + +const ADD_0_1: [u64; 4] = + [0x4b5eae0644dbbb95, 0x54e726fa5a35973e, 0x0fbe0c46da68786e, 0x91aca12c2f1fc484]; +const SUB_0_1: [u64; 4] = + [0x439ab29586207b8f, 0x17716cf6fe3a7c7f, 0xffd1444b2cedbec5, 0x3f98de1b15a3d8be]; +const MUL_0_1: [u64; 4] = + [0x2a5bc077905603d1, 0x0279a77a67d9f751, 0xe79bb660d346023f, 0x18a7b810e9f8c46b]; +const ADD_0_2: [u64; 4] = + [0x83b17237be9c2069, 0x4f544f5dcfb16319, 0xb6bfe052bbdc9882, 0x8f526a9ae4303003]; +const SUB_0_2: [u64; 4] = + [0x0b47ee640c6016bb, 0x1d04449388beb0a4, 0x58cf703f4b799eb1, 0x41f314ac60936d3f]; +const MUL_0_2: [u64; 4] = + [0x9149a980bb219504, 0x018fdddd856d04af, 0x9de5e0ad83e7b09b, 0x3d6ce1dc1e08b32e]; +const ADD_1_2: [u64; 4] = + [0x4016bfa2387ba4da, 0x37e2e266d176e69a, 0xb6ee9c078eeed9bd, 0x4fb98c7fce8c5744]; +const SUB_1_2: [u64; 4] = + [0xc7ad3bce863f9b2c, 0x0592d79c8a843424, 0x58fe2bf41e8bdfec, 0x025a36914aef9480]; +const MUL_1_2: [u64; 4] = + [0x2f36d4e288f5f11e, 0xc063c1277a4f5a4b, 0xa1cc3a541c11f76d, 0xb84da028335accb8]; + +const NEG_0: [u64; 4] = + [0xf855ae3eeab825af, 0x048292ee0310965c, 0x783857b6fc54e465, 0x975d405c5d9e315e]; +const INV_0: [u64; 4] = + [0x5caf1566d4a30b98, 0x02516ac43cbc4edd, 0xf39fa3889f4727f5, 0x7b473842b2a3515e]; +const NEG_1: [u64; 4] = + [0x3bf060d470d8a13e, 0x1bf3ffe5014b12dc, 0x78099c022942a32a, 0xd6f61e7773420a1d]; +const INV_1: [u64; 4] = + [0x47d7cc33ae095bf8, 0xd67d5cc78acede87, 0xebba12ce17ad83be, 0x2f52e24b1ab3e925]; +const NEG_2: [u64; 4] = + [0x039d9ca2f7183c6a, 0x2186d7818bcf4701, 0xd107c7f647ce8316, 0xd9505508be319e9d]; +const INV_2: [u64; 4] = + [0x4087e71103f99cb2, 0xc0e93d9c142f8435, 0x51ede30026aea71a, 0x8eb5e166fb519e3a]; + +fn fe(limbs: [u64; 4]) -> P256K1ScalarField { + P256K1ScalarField::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + let pairs: [(usize, usize, [u64; 4], [u64; 4], [u64; 4]); 3] = [ + (0, 1, ADD_0_1, SUB_0_1, MUL_0_1), + (0, 2, ADD_0_2, SUB_0_2, MUL_0_2), + (1, 2, ADD_1_2, SUB_1_2, MUL_1_2), + ]; + for (i, j, a, s, m) in pairs { + assert_eq!(vals[i].add(&vals[j]), fe(a), "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), fe(a), "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), fe(s), "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), fe(m), "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), fe(m), "mul is commutative ({j},{i})"); + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2]; + let negs = [NEG_0, NEG_1, NEG_2]; + let invs = [INV_0, INV_1, INV_2]; + for i in 0..3 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + for &v in &vals { + assert_eq!(v.add(&P256K1ScalarField::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&P256K1ScalarField::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), P256K1ScalarField::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), P256K1ScalarField::ZERO, "x + (-x) == 0"); + assert_eq!(v.mul(&v.invert()), P256K1ScalarField::ONE, "x * x^-1 == 1"); + } + assert_eq!( + P256K1ScalarField::ZERO.invert(), + P256K1ScalarField::ZERO, + "0^-1 == 0 by convention" + ); +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..4 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + assert_eq!(fe(N_LIMBS), P256K1ScalarField::ZERO); +} + +#[test] +fn secret_scalar_reduces_and_round_trips() { + let secret = P256K1Scalar::from_limbs(VALS_0); + assert_eq!(P256K1ScalarField::from_secret(&secret), fe(VALS_0)); +} + +#[test] +fn secret_scalar_reduces_out_of_range_input() { + let secret = P256K1Scalar::from_limbs(N_LIMBS); + assert_eq!(P256K1ScalarField::from_secret(&secret), P256K1ScalarField::ZERO); +} + +#[test] +fn secret_scalar_be_bytes_round_trip() { + for limbs in [VALS_0, VALS_1, VALS_2] { + let secret = P256K1Scalar::from_limbs(limbs); + let round_tripped = P256K1Scalar::from_be_bytes(&secret.to_be_bytes()); + assert_eq!(round_tripped, secret); + } +} + +#[test] +fn public_scalar_round_trips_and_reduces() { + for limbs in [VALS_0, VALS_1, VALS_2] { + assert_eq!(P256K1PublicScalar::from_limbs(limbs).to_limbs(), limbs); + } + assert_eq!(P256K1PublicScalar::from_limbs(N_LIMBS).to_limbs(), [0; 4]); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the P-256 field test's property check. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0xBB67AE8584CAA73B); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != P256K1ScalarField::ZERO { + assert_eq!(a.mul(&a.invert()), P256K1ScalarField::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE000000000A); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 4], + { + let mut l = [0u64; 4]; + l[0] = 1; + l + }, + [u64::MAX; 4], + { + let mut l = [u64::MAX; 4]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `zeroize` is the signing path's way of not leaving `d`, `k` or `k^-1` legible on the stack +/// (see the method's own docs for why this type is not wrapped in `Secret` instead). Whether the +/// volatile write survives optimization cannot be observed from Rust; that the value is actually +/// cleared can be, and is what would break if the method were ever reduced to a no-op. +#[test] +fn zeroize_clears_the_value() { + let mut a = fe([1, 2, 3, 4]); + assert_ne!(a, P256K1ScalarField::ZERO, "precondition: the value starts non-zero"); + a.zeroize(); + assert_eq!(a, P256K1ScalarField::ZERO, "zeroize must leave the value at zero"); + + // and it is idempotent, so a caller scrubbing twice on overlapping paths is harmless + a.zeroize(); + assert_eq!(a, P256K1ScalarField::ZERO); +} + +/// `is_zero` on the scalar field, pinned for both truth values and across every limb position +/// (see the field test of the same name for why); it is the check behind `negate`'s `0 -> 0` +/// special case. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(P256K1ScalarField::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::p256k1_scalar::N_LIMBS).is_zero().to_bool(), "n reduces to 0"); + assert!(!P256K1ScalarField::ONE.is_zero().to_bool()); + for limb_idx in 0..4 { + let mut limbs = [0u64; 4]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `n - 1`, the largest canonical scalar, is the worst case for REDC's final conditional +/// subtraction and for `add`'s carry correction, and was absent from every known-answer set +/// above (whose values are pseudorandom, so never near `n`). Expected values follow from +/// `(n-1)^2 == 1` and `(n-1) + (n-1) == n - 2` in the field, computed from `n` in Python. +#[test] +fn known_answer_at_n_minus_1() { + let n_minus_1_limbs: [u64; 4] = + [0xbfd25e8cd0364140, 0xbaaedce6af48a03b, 0xfffffffffffffffe, 0xffffffffffffffff]; + let n_minus_1 = fe(n_minus_1_limbs); + let n_minus_2 = + fe([0xbfd25e8cd036413f, 0xbaaedce6af48a03b, 0xfffffffffffffffe, 0xffffffffffffffff]); + + assert_eq!(n_minus_1.to_limbs(), n_minus_1_limbs, "Montgomery round trip of n-1"); + assert_eq!(P256K1PublicScalar::from_limbs(n_minus_1_limbs).to_limbs(), n_minus_1_limbs); + assert_eq!(n_minus_1.add(&n_minus_1), n_minus_2, "(n-1) + (n-1) == n-2"); + assert_eq!(n_minus_1.add(&P256K1ScalarField::ONE), P256K1ScalarField::ZERO, "(n-1) + 1 == 0"); + assert_eq!(P256K1ScalarField::ZERO.sub(&P256K1ScalarField::ONE), n_minus_1, "0 - 1 == n-1"); + assert_eq!(n_minus_1.mul(&n_minus_1), P256K1ScalarField::ONE, "(n-1)^2 == 1"); + assert_eq!(n_minus_1.square(), P256K1ScalarField::ONE, "(n-1)^2 == 1, via square"); + assert_eq!(n_minus_1.negate(), P256K1ScalarField::ONE, "-(n-1) == 1"); + assert_eq!(n_minus_1.invert(), n_minus_1, "(n-1)^-1 == n-1"); + assert_eq!(P256K1ScalarField::ONE.invert(), P256K1ScalarField::ONE, "1^-1 == 1"); +} + +/// The variable-time inverse on the public scalar type must agree with the constant-time +/// [`P256K1ScalarField::invert`] everywhere: on the known-answer values (whose inverses are pinned above +/// from Python), on `0` (both return `0` by convention), `1`, `n - 1`, and on pseudorandom +/// values -- including ones with long runs of zero bits, which are where the binary algorithm +/// takes its longest halving chains. +#[test] +fn public_scalar_invert_vartime_agrees_with_constant_time_invert() { + let n_minus_1 = { + let mut l = bouncycastle_ec::p256k1_scalar::N_LIMBS; + l[0] -= 1; + l + }; + let mut one = [0u64; 4]; + one[0] = 1; + // Values whose low limb is exactly 1 with higher limbs set: odd, so the binary algorithm must + // not halve them, yet indistinguishable from `1` by their low limb alone. A halving test that + // looked at more than the lowest bit (or at the wrong limb) breaks the `u == x1 * a` invariant + // on exactly these and nowhere the pseudorandom inputs below would reach. + let mut low_limb_one_a = [0u64; 4]; + low_limb_one_a[0] = 1; + low_limb_one_a[1] = 1; + let mut low_limb_one_b = [0u64; 4]; + low_limb_one_b[0] = 1; + low_limb_one_b[3] = 1; + let mut low_limb_one_c = [0u64; 4]; + low_limb_one_c[0] = 1; + low_limb_one_c[1] = u64::MAX; + let mut fixed: Vec<[u64; 4]> = + vec![[0u64; 4], one, n_minus_1, low_limb_one_a, low_limb_one_b, low_limb_one_c]; + fixed.extend([VALS_0, VALS_1, VALS_2]); + for limbs in fixed { + assert_eq!( + bouncycastle_ec::p256k1_scalar::P256K1PublicScalar::from_limbs(limbs).invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } + assert_eq!( + bouncycastle_ec::p256k1_scalar::P256K1PublicScalar::from_limbs([0u64; 4]).invert_vartime(), + P256K1ScalarField::ZERO, + "0^-1 == 0 by convention, matching invert" + ); + + let mut rng = Xorshift64(0x1AC0_B5EC_0000_0004); + for i in 0..500 { + let mut limbs = rng.next_limbs(); + if i % 4 == 0 { + // Sparse values: clear the low half of each limb so `u`/`v` shed many bits per step. + for limb in limbs.iter_mut() { + *limb &= 0xffff_ffff_0000_0000; + } + } + assert_eq!( + bouncycastle_ec::p256k1_scalar::P256K1PublicScalar::from_limbs(limbs).invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } +} diff --git a/crypto/ec/tests/p256k1_sec1_tests.rs b/crypto/ec/tests/p256k1_sec1_tests.rs new file mode 100644 index 000000000..cef0daeb1 --- /dev/null +++ b/crypto/ec/tests/p256k1_sec1_tests.rs @@ -0,0 +1,134 @@ +//! Known-answer tests for SEC 1 §2.3.3/§2.3.4 point encoding and decoding, secp256k1. +//! +//! `G`'s coordinates are SEC 2 v2 §2.4.1's (already verified independently in +//! `p256k1_point_tests.rs`/`p256k1_comb_tests.rs`); `6G`'s were computed independently in Python via +//! the standard affine group law (`a = 0`) specifically because it has odd `y` where `G`'s is even, +//! so between them both compressed tags (`0x02`/`0x03`) are exercised. The encodings below were +//! derived from those coordinates independently in Python via plain big-endian byte packing. + +use bouncycastle_ec::p256k1::P256K1FieldElement; +use bouncycastle_ec::p256k1_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p256k1_sec1::{decode, decode_point, encode_compressed, encode_uncompressed}; + +const SIX_G_X: [u64; 4] = + [0x2f057a1460297556, 0x82f6472f8568a18b, 0x20453a14355235d3, 0xfff97bd5755eeea4]; +const SIX_G_Y: [u64; 4] = + [0x3c870c36b075f297, 0xde80f0f6518fe4a0, 0xf3be96017f45c560, 0xae12777aacfbb620]; + +const UNCOMPRESSED_G: [u8; 65] = [ + 0x04, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, 0x87, 0x0b, + 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, + 0x98, 0x48, 0x3a, 0xda, 0x77, 0x26, 0xa3, 0xc4, 0x65, 0x5d, 0xa4, 0xfb, 0xfc, 0x0e, 0x11, 0x08, + 0xa8, 0xfd, 0x17, 0xb4, 0x48, 0xa6, 0x85, 0x54, 0x19, 0x9c, 0x47, 0xd0, 0x8f, 0xfb, 0x10, 0xd4, + 0xb8, +]; + +const COMPRESSED_G: [u8; 33] = [ + 0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, 0x87, 0x0b, + 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, + 0x98, +]; + +const UNCOMPRESSED_SIX_G: [u8; 65] = [ + 0x04, 0xff, 0xf9, 0x7b, 0xd5, 0x75, 0x5e, 0xee, 0xa4, 0x20, 0x45, 0x3a, 0x14, 0x35, 0x52, 0x35, + 0xd3, 0x82, 0xf6, 0x47, 0x2f, 0x85, 0x68, 0xa1, 0x8b, 0x2f, 0x05, 0x7a, 0x14, 0x60, 0x29, 0x75, + 0x56, 0xae, 0x12, 0x77, 0x7a, 0xac, 0xfb, 0xb6, 0x20, 0xf3, 0xbe, 0x96, 0x01, 0x7f, 0x45, 0xc5, + 0x60, 0xde, 0x80, 0xf0, 0xf6, 0x51, 0x8f, 0xe4, 0xa0, 0x3c, 0x87, 0x0c, 0x36, 0xb0, 0x75, 0xf2, + 0x97, +]; + +const COMPRESSED_SIX_G: [u8; 33] = [ + 0x03, 0xff, 0xf9, 0x7b, 0xd5, 0x75, 0x5e, 0xee, 0xa4, 0x20, 0x45, 0x3a, 0x14, 0x35, 0x52, 0x35, + 0xd3, 0x82, 0xf6, 0x47, 0x2f, 0x85, 0x68, 0xa1, 0x8b, 0x2f, 0x05, 0x7a, 0x14, 0x60, 0x29, 0x75, + 0x56, +]; + +fn fe(limbs: [u64; 4]) -> P256K1FieldElement { + P256K1FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_encode_uncompressed() { + assert_eq!(encode_uncompressed(&fe(G_X_LIMBS), &fe(G_Y_LIMBS)), UNCOMPRESSED_G); + assert_eq!(encode_uncompressed(&fe(SIX_G_X), &fe(SIX_G_Y)), UNCOMPRESSED_SIX_G); +} + +#[test] +fn known_answer_encode_compressed() { + assert_eq!( + encode_compressed(&fe(G_X_LIMBS), &fe(G_Y_LIMBS)), + COMPRESSED_G, + "G has even y -> tag 0x02" + ); + assert_eq!( + encode_compressed(&fe(SIX_G_X), &fe(SIX_G_Y)), + COMPRESSED_SIX_G, + "6G has odd y -> tag 0x03" + ); +} + +#[test] +fn known_answer_decode_uncompressed() { + let (x, y) = decode(&UNCOMPRESSED_G).expect("valid uncompressed point"); + assert_eq!(x, fe(G_X_LIMBS)); + assert_eq!(y, fe(G_Y_LIMBS)); +} + +#[test] +fn known_answer_decode_compressed() { + let (x, y) = decode(&COMPRESSED_G).expect("valid compressed point, tag 0x02"); + assert_eq!(x, fe(G_X_LIMBS)); + assert_eq!(y, fe(G_Y_LIMBS)); + + let (x6, y6) = decode(&COMPRESSED_SIX_G).expect("valid compressed point, tag 0x03"); + assert_eq!(x6, fe(SIX_G_X)); + assert_eq!(y6, fe(SIX_G_Y)); +} + +#[test] +fn round_trip_uncompressed_and_compressed() { + for (x, y) in [(G_X_LIMBS, G_Y_LIMBS), (SIX_G_X, SIX_G_Y)] { + let uncompressed = encode_uncompressed(&fe(x), &fe(y)); + assert_eq!(decode(&uncompressed), Some((fe(x), fe(y)))); + + let compressed = encode_compressed(&fe(x), &fe(y)); + assert_eq!(decode(&compressed), Some((fe(x), fe(y)))); + + assert!(decode_point(&uncompressed).unwrap().to_affine().is_some()); + } +} + +#[test] +fn decode_rejects_malformed_input() { + assert_eq!(decode(&[]), None); + assert_eq!(decode(&[0x04; 64]), None); + assert_eq!(decode(&[0x04; 66]), None); + assert_eq!(decode(&[0x02; 16]), None); + + let mut bad_tag_65 = UNCOMPRESSED_G; + bad_tag_65[0] = 0x02; + assert_eq!(decode(&bad_tag_65), None); + + let mut bad_tag_33 = COMPRESSED_G; + bad_tag_33[0] = 0x04; + assert_eq!(decode(&bad_tag_33), None); + + assert_eq!(decode(&[0x00]), None); + + let mut x_too_big = COMPRESSED_G; + x_too_big[1..33].fill(0xff); + assert_eq!(decode(&x_too_big), None); + + let mut no_root = COMPRESSED_G; + no_root[1..33].fill(0); + no_root[32] = 5; // x = 5, confirmed (in the Python derivation) to have no square root + assert_eq!(decode(&no_root), None); + + let mut not_on_curve = UNCOMPRESSED_G; + not_on_curve[64] ^= 0x01; + assert_eq!(decode(¬_on_curve), None); + + let mut y_too_big = UNCOMPRESSED_G; + y_too_big[33..65].fill(0xff); + assert_eq!(decode(&y_too_big), None); +} diff --git a/crypto/ec/tests/p256k1_wnaf_tests.rs b/crypto/ec/tests/p256k1_wnaf_tests.rs new file mode 100644 index 000000000..ed351f8fe --- /dev/null +++ b/crypto/ec/tests/p256k1_wnaf_tests.rs @@ -0,0 +1,147 @@ +//! Known-answer tests for [`shamir_multiply`] (`[u]G + [v]Q`), secp256k1. +//! +//! `Q` below is `d*G` for a fixed test-only `d` (not a real key). Expected results were computed +//! independently in Python from the affine group law (`a = 0`), the same way as +//! `p256k1_comb_tests.rs`, with `random.seed(808384)`. + +use bouncycastle_ec::p256k1::P256K1FieldElement; +use bouncycastle_ec::p256k1_comb::comb_multiply_base_point; +use bouncycastle_ec::p256k1_point::P256K1JacobianPoint; +use bouncycastle_ec::p256k1_scalar::{P256K1PublicScalar, P256K1Scalar}; +use bouncycastle_ec::p256k1_wnaf::shamir_multiply; + +const Q_X: [u64; 4] = + [0x0ea6f07bd759d339, 0xb96d88cc46f523cb, 0x20ca70079f805400, 0x4b0096e546d82613]; +const Q_Y: [u64; 4] = + [0xa8ce78f807726fe5, 0x16a050fb2e947cd2, 0xabd80676941bde4a, 0x007c58c019edf2a3]; + +fn q() -> P256K1JacobianPoint { + P256K1JacobianPoint::from_affine( + P256K1FieldElement::from_limbs(Q_X), + P256K1FieldElement::from_limbs(Q_Y), + ) +} + +fn scalar(limbs: [u64; 4]) -> P256K1PublicScalar { + P256K1PublicScalar::from_limbs(limbs) +} + +fn assert_result_is(u: [u64; 4], v: [u64; 4], expected_x: [u64; 4], expected_y: [u64; 4]) { + let result = shamir_multiply(&scalar(u), &scalar(v), &q()); + let (rx, ry) = result.to_affine().expect("expected a non-infinity result"); + assert_eq!(rx, P256K1FieldElement::from_limbs(expected_x), "x mismatch for u={u:x?} v={v:x?}"); + assert_eq!(ry, P256K1FieldElement::from_limbs(expected_y), "y mismatch for u={u:x?} v={v:x?}"); +} + +#[test] +fn known_answer_large_u_v() { + let u: [u64; 4] = + [0xc8c7279f85b5b57e, 0xdf4dc003c482b49d, 0xcf9d0a68d7fd9586, 0x72c8c95608f4eb96]; + let v: [u64; 4] = + [0x48403aad19a04ae7, 0x74cc2df91a4a3ac6, 0xa73e462c3bc5bf0c, 0x838e541242a28502]; + assert_result_is( + u, + v, + [0xd185e96f95c8b444, 0x4921c6651314526c, 0x38dda3e43e50e5bf, 0xe2df93a43c571e20], + [0xb4f92d6e47a72846, 0xfb2aa735ae915e40, 0x970c60619d1cdd81, 0x21401303f02495dd], + ); +} + +#[test] +fn edge_case_v_is_zero_reduces_to_u_times_g() { + assert_result_is( + [5, 0, 0, 0], + [0, 0, 0, 0], + [0xcba8d569b240efe4, 0xe88b84bddc619ab7, 0x55b4a7250a5c5128, 0x2f8bde4d1a072093], + [0xdca87d3aa6ac62d6, 0xf788271bab0d6840, 0xd4dba9dda6c9c426, 0xd8ac222636e5e3d6], + ); +} + +#[test] +fn edge_case_u_is_zero_reduces_to_v_times_q() { + assert_result_is( + [0, 0, 0, 0], + [5, 0, 0, 0], + [0xd6c3a13d8c58cf02, 0xce9fd201290122ed, 0x862cfc348c1b578d, 0x1c2dead1b2b00ca4], + [0x5b8af69af5dcf730, 0x0464a19ce8f84a23, 0xd163788cfd503a3c, 0x0dce7d767a5e38c7], + ); +} + +#[test] +fn edge_case_both_zero_is_infinity() { + let result = shamir_multiply(&scalar([0; 4]), &scalar([0; 4]), &q()); + assert!(result.to_affine().is_none()); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn cross_checked_against_the_comb_multiplier_over_many_pseudorandom_scalars() { + // shamir_multiply(k, 0, anything) must equal comb_multiply_base_point(k): two independently + // implemented multipliers (fixed-base comb vs. interleaved wNAF) computing the same [k]G. + let mut rng = Xorshift64(0xE7C558A6A6E7F1DE); + for _ in 0..500 { + let k_limbs = rng.next_limbs(); + let via_comb = comb_multiply_base_point(&P256K1Scalar::from_limbs(k_limbs)); + let via_wnaf = + shamir_multiply(&P256K1PublicScalar::from_limbs(k_limbs), &scalar([0; 4]), &q()); + assert_eq!(via_comb.to_affine(), via_wnaf.to_affine(), "k = {k_limbs:x?}"); + } +} + +/// Every other test in this file uses one fixed `Q = dG`. The three `Q` values here each take a +/// path that one cannot: `Q = infinity` makes every `[v]Q` digit an addition of the identity, +/// `Q = G` makes the two precomputed odd-multiple tables identical (so the interleaved +/// additions hit the same-point case of `add_vartime`), and `Q = -G` makes them negatives of +/// each other (the opposite-point case, and with `u == v` a final result of infinity). Expected +/// values come from the fixed-base comb multiplier on the combined scalar, which is pinned by +/// its own known-answer tests. +#[test] +fn edge_case_q_is_infinity_g_or_minus_g() { + let g = P256K1JacobianPoint::from_affine( + P256K1FieldElement::from_limbs(bouncycastle_ec::p256k1_domain::G_X_LIMBS), + P256K1FieldElement::from_limbs(bouncycastle_ec::p256k1_domain::G_Y_LIMBS), + ); + let mut u = [0u64; 4]; + u[0] = 12345; + let mut v = [0u64; 4]; + v[0] = 6789; + let mut u_plus_v = [0u64; 4]; + u_plus_v[0] = 19134; + let mut u_minus_v = [0u64; 4]; + u_minus_v[0] = 5556; + let via_comb = |k: [u64; 4]| comb_multiply_base_point(&P256K1Scalar::from_limbs(k)).to_affine(); + + // [u]G + [v]infinity == [u]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &P256K1JacobianPoint::INFINITY).to_affine(), + via_comb(u) + ); + // [u]G + [v]G == [u+v]G + assert_eq!(shamir_multiply(&scalar(u), &scalar(v), &g).to_affine(), via_comb(u_plus_v)); + // [u]G + [v](-G) == [u-v]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &g.negate()).to_affine(), + via_comb(u_minus_v) + ); + // [u]G + [u](-G) == infinity + assert!(shamir_multiply(&scalar(u), &scalar(u), &g.negate()).is_infinity().to_bool()); + // [0]G + [v]G == [v]G: the G table is never consulted, only Q's + assert_eq!(shamir_multiply(&scalar([0u64; 4]), &scalar(v), &g).to_affine(), via_comb(v)); +} diff --git a/crypto/ec/tests/p384_comb_tests.rs b/crypto/ec/tests/p384_comb_tests.rs new file mode 100644 index 000000000..9e8f3ee1b --- /dev/null +++ b/crypto/ec/tests/p384_comb_tests.rs @@ -0,0 +1,95 @@ +//! Known-answer tests for [`comb_multiply_base_point`] (`[k]G`), P-384. +//! +//! `k = 2, 3` reuse the values verified independently in `p384_point_tests.rs`; `k = n-1` (where +//! `[k]G = -G`) was computed the same way (Python affine group law), `random.seed` not applicable +//! since it's a fixed edge case. + +use bouncycastle_ec::p384::P384FieldElement; +use bouncycastle_ec::p384_comb::comb_multiply_base_point; +use bouncycastle_ec::p384_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p384_scalar::P384Scalar; + +fn scalar(limbs: [u64; 6]) -> P384Scalar { + P384Scalar::from_limbs(limbs) +} + +fn assert_result_is(k_limbs: [u64; 6], x: [u64; 6], y: [u64; 6]) { + let result = comb_multiply_base_point(&scalar(k_limbs)); + let (rx, ry) = result.to_affine().expect("[k]G must not be infinity for k in (0, n)"); + assert_eq!(rx, P384FieldElement::from_limbs(x), "x mismatch for k = {k_limbs:x?}"); + assert_eq!(ry, P384FieldElement::from_limbs(y), "y mismatch for k = {k_limbs:x?}"); +} + +#[test] +fn known_answer_small_k() { + assert_result_is([1, 0, 0, 0, 0, 0], G_X_LIMBS, G_Y_LIMBS); + assert_result_is( + [2, 0, 0, 0, 0, 0], + [ + 0x5b96a9c75295df61, 0x4fe0e86ebe0e64f8, 0x51d207d19fb96e9e, 0x89025959a6f434d6, + 0x69260045c55b97f0, 0x08d999057ba3d2d9, + ], + [ + 0x61501e700a940e80, 0x5ffd43e94d39e22d, 0x904e505f256ab425, 0xb275d875bc6cc43e, + 0xb7bfe8dffd6dba74, 0x8e80f1fa5b1b3ced, + ], + ); + assert_result_is( + [3, 0, 0, 0, 0, 0], + [ + 0x02d7e5c70500c831, 0xb408bbae5026580d, 0xbea4f240d3566da6, 0xcb9d3910202dcd06, + 0x64793c7e5fdc7d98, 0x077a41d4606ffa14, + ], + [ + 0xb65f28600a2f1df1, 0xc24abd6be4b5d298, 0xf7684c0edc111eac, 0x8520b41c85115aa5, + 0x7d0bbe9602a9fc99, 0xc995f7ca0b0c4283, + ], + ); +} + +#[test] +fn known_answer_large_k() { + let k: [u64; 6] = [ + 0x1c80317fa3b1799e, 0xbdd640fb06671ad1, 0x3eb13b9046685257, 0x23b8c1e9392456de, + 0x1a3d1fa7bc8960a9, 0xbd9c66b3ad3c2d6d, + ]; + assert_result_is( + k, + [ + 0x7dd57ff886515cdc, 0x1c8525be6c64cc60, 0xefed50c6fe1a5b18, 0x251871a6fb5bccea, + 0x1c65ee00efce13bc, 0x0c10d4b4eeddcc19, + ], + [ + 0x74f5e7f52e41abc6, 0x8d3ba3290d37622d, 0x6974925761e35ace, 0x688dd2ead26f7c83, + 0xdec652bf66370892, 0x036b7085ab9c77bd, + ], + ); +} + +#[test] +fn edge_case_k_equals_n_minus_1() { + // (n-1)*G == -G: same x as G, negated y. + let n_minus_1: [u64; 6] = [ + 0xecec196accc52972, 0x581a0db248b0a77a, 0xc7634d81f4372ddf, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, + ]; + assert_result_is( + n_minus_1, + G_X_LIMBS, + [ + 0x85bce2846f15f1a0, 0xf59f4e30e2817e62, 0x1625ceec4a0f473e, 0x070be242d765eb83, + 0xa26167406d6d23d6, 0xc9e821b569d9d390, + ], + ); +} + +/// `k = 0` is the one scalar for which `[k]G` is the identity: every comb digit is `0`, so every +/// round selects table entry `0` (the infinity sentinel) and the accumulator never leaves +/// `INFINITY`. Nothing in the signing path can produce it (`k` is in `[1, n-1]` by construction), +/// which is exactly why it needs pinning here rather than being left to chance. +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let result = comb_multiply_base_point(&scalar([0u64; 6])); + assert!(result.is_infinity().to_bool()); + assert!(result.to_affine().is_none()); +} diff --git a/crypto/ec/tests/p384_extra_bits_tests.rs b/crypto/ec/tests/p384_extra_bits_tests.rs new file mode 100644 index 000000000..77931893b --- /dev/null +++ b/crypto/ec/tests/p384_extra_bits_tests.rs @@ -0,0 +1,159 @@ +//! Known-answer tests for [`reduce_wide_bits_mod_n_minus_1`] (FIPS 186-5 Appendix A.4.1 steps +//! 3-5): `x = bits2int(X)`, `x = x mod (n-1)`, `x = x + 1`. Expected values computed independently +//! in Python via `(int.from_bytes(bytes, 'big') % (n - 1)) + 1` with `n` from SP 800-186 +//! §3.2.1.4, not derived from this crate's own reduction code -- see `extra_bits_tests.rs`'s docs +//! for why this independent-reference pinning matters. Inputs are the 56 bytes (448 bits) +//! `keys_p384` and `ecdsa_p384` actually draw, except where a shorter input is the point. + +use bouncycastle_ec::p384_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::p384_scalar::N_LIMBS; +use bouncycastle_ec::p384_sec1::be_bytes_from_limbs; +use bouncycastle_hex::decode as hex_decode; + +fn bytes48(hex: &str) -> [u8; 48] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +#[test] +fn known_answer_repeating_pattern() { + let input: Vec = [0x01u8, 0x02, 0x03, 0x04].iter().cloned().cycle().take(56).collect(); + let expected = bytes48( + "01020304010203040102030401020304013b119a9ccd3619eb1707bbd346539086279eb3ecc7299aa3ee0276b98d073d", + ); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_ff() { + let input = [0xffu8; 56]; + let expected = bytes48( + "00000000000000000000000000000000389cb27e0bc8d220a7e5f24db74f58851313e695333ad68e0000000000000000", + ); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_zero_yields_one() { + // bits2int(0) = 0; 0 mod (n-1) = 0; +1 = 1 -- the smallest value Appendix A.4.1 can produce. + let input = [0u8; 56]; + let mut expected = [0u8; 48]; + expected[47] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +/// An `n`-width input below `n - 1` is its own residue, so the result is `x + 1`: the case the +/// single-subtraction reduction this module replaced was built for, kept as a check that the +/// wide reduction agrees with it on its home ground. `x` is pseudorandom, from Python. +#[test] +fn known_answer_n_width_input_below_n_minus_1() { + let x = bytes48( + "0c479ba61fe9eb497e4ef8d29646bba8128fd0fd853321fe2e841ff3542270c0a4afd95a574e5cebe7531273ac31909b", + ); + let mut x_plus_1 = x; + x_plus_1[47] += 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&x).to_be_bytes(), x_plus_1); +} + +/// The boundaries of Appendix A.4.1's own arithmetic: `n - 1` is the modulus of step 4, so it +/// reduces to `0` and step 5 makes it `1`; `n - 2` is the largest residue, so it comes out as +/// `n - 1`, the top of the output interval; and `n` itself is `1 mod (n-1)`, so `2`. +#[test] +fn known_answer_at_the_n_minus_1_boundary() { + let n = be_bytes_from_limbs(&N_LIMBS); + let mut n_minus_1 = n; + n_minus_1[47] -= 1; // n is odd, so neither subtraction borrows + let mut n_minus_2 = n; + n_minus_2[47] -= 2; + + let mut one = [0u8; 48]; + one[47] = 1; + let mut two = [0u8; 48]; + two[47] = 2; + + let mut input = [0u8; 56]; + input[8..].copy_from_slice(&n_minus_1); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), one, "n - 1 -> 1"); + input[8..].copy_from_slice(&n_minus_2); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), n_minus_1, "n - 2 -> n - 1"); + input[8..].copy_from_slice(&n); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), two, "n -> 2"); +} + +/// The bit-serial algorithm this module implemented before switching to Barrett reduction, kept +/// here as an independent reference: Horner's rule over the input bits, doubling the running +/// remainder and subtracting `n - 1` when it is exceeded, with the carry out of the top limb folded +/// back in as `2^384 mod (n-1) = 2^384 - (n-1)`. Slow, obviously correct, and sharing no +/// code with the Barrett path beyond `nat`'s add and sub. +fn reference_bit_serial(bytes: &[u8]) -> [u8; 48] { + use bouncycastle_ec::nat; + let n = bouncycastle_ec::p384_scalar::N_LIMBS; + let mut m = n; + m[0] -= 1; // n is odd + let two_pow_mod_m = nat::sub(&[0u64; 6], &m).0; // 2^384 - m, wrapping + let mut acc = [0u64; 6]; + for &byte in bytes { + for bit_idx in (0..8).rev() { + let (mut doubled, carry) = nat::add(&acc, &acc); + doubled[0] |= ((byte >> bit_idx) & 1) as u64; + if carry == 1 { + doubled = nat::add(&doubled, &two_pow_mod_m).0; + } + let (reduced, borrow) = nat::sub(&doubled, &m); + acc = if borrow == 1 { doubled } else { reduced }; + } + } + let plus_one = nat::add(&acc, &[1u64, 0, 0, 0, 0, 0]).0; + bouncycastle_ec::p384_sec1::be_bytes_from_limbs(&plus_one) +} + +/// xorshift64*, fixed seed, as in the `ec` crate's property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } +} + +/// Barrett against the bit-serial reference over pseudorandom inputs at every width from one byte +/// up to the 95-byte maximum the function accepts, biased towards the widths actually drawn +/// (56 bytes) and towards values with long runs of set bits, where Barrett's quotient estimate is +/// furthest off and the most conditional subtractions are needed. +#[test] +fn agrees_with_the_bit_serial_reference_over_many_pseudorandom_inputs() { + let mut rng = Xorshift64(0xBA77E770_0000_0006); + for i in 0..3000 { + let len = match i % 4 { + 0 => 56, + 1 => 95, + _ => 1 + (rng.next_u64() as usize % 95), + }; + let mut bytes = vec![0u8; len]; + for chunk in bytes.chunks_mut(8) { + let word = rng.next_u64().to_be_bytes(); + chunk.copy_from_slice(&word[..chunk.len()]); + } + if i % 3 == 0 { + for b in bytes.iter_mut().take(len / 2) { + *b = 0xff; + } + } + assert_eq!( + reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), + reference_bit_serial(&bytes), + "len = {len}, bytes = {bytes:02x?}" + ); + } +} + +#[test] +fn maximum_width_all_ones_input() { + // 2^760 - 1, the largest input accepted: Barrett's precondition holds with room to spare. + let bytes = [0xffu8; 95]; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), reference_bit_serial(&bytes)); +} diff --git a/crypto/ec/tests/p384_field_tests.rs b/crypto/ec/tests/p384_field_tests.rs new file mode 100644 index 000000000..453d593c4 --- /dev/null +++ b/crypto/ec/tests/p384_field_tests.rs @@ -0,0 +1,454 @@ +//! Known-answer tests for [`P384FieldElement`] arithmetic. +//! +//! `p = 2**384 - 2**128 - 2**96 + 2**32 - 1` has no dedicated NIST-published field-arithmetic KAT +//! suite (SP 800-186 gives only the domain parameters). Expected values computed independently in +//! Python (arbitrary-precision integers, not from recall): +//! +//! ```text +//! p = 2**384 - 2**128 - 2**96 + 2**32 - 1 +//! random.seed(4384) +//! vals = [random.randrange(1, p) for _ in range(4)] + [0] +//! # for each pair (i, j) with i < j: (vals[i]+vals[j]) % p, (vals[i]-vals[j]) % p, (vals[i]*vals[j]) % p +//! # for each i: (-vals[i]) % p, pow(vals[i], p-2, p) if vals[i] != 0 else 0 +//! ``` + +use bouncycastle_ec::p384::P384FieldElement; + +const VALS_0: [u64; 6] = [ + 0xfbf0df09ff15d76c, 0xf5a7ffb01c944ad6, 0x59c184f6c245b0b0, 0xf42a4e0449f2cb85, + 0xc1b0f9376ef449ac, 0xb79e039ea01cee3b, +]; +const VALS_1: [u64; 6] = [ + 0x84e8350c9b1affa7, 0x836c0706d0c1f027, 0xede2c0b840684e77, 0x911bfe952a88e6e6, + 0xa445a43dfcf34065, 0x822c517432ee5368, +]; +const VALS_2: [u64; 6] = [ + 0x9a2952cc4df065e5, 0xb460151e3cb7e277, 0x8e0af308d3908b9f, 0x594193102e6ede9b, + 0x33c2fe4a458505c6, 0x30b0270c28a08938, +]; +const VALS_3: [u64; 6] = [ + 0xbe4717b569c8b2d8, 0x3af87fdc818bb903, 0x0dcbe57fa3200615, 0x42d228f2780bea9c, + 0x6c7715376e8b494b, 0xece924a7b60565e1, +]; +const VALS_4: [u64; 6] = [0, 0, 0, 0, 0, 0]; + +const ADD_0_1: [u64; 6] = [ + 0x80d914159a30d714, 0x791406b7ed563afe, 0x47a445af02adff29, 0x85464c99747bb26c, + 0x65f69d756be78a12, 0x39ca5512d30b41a4, +]; +const SUB_0_1: [u64; 6] = [ + 0x7708a9fd63fad7c5, 0x723bf8a94bd25aaf, 0x6bdec43e81dd6239, 0x630e4f6f1f69e49e, + 0x1d6b54f972010947, 0x3571b22a6d2e9ad3, +]; +const MUL_0_1: [u64; 6] = [ + 0xf7fbd6cf75cb7360, 0xd0645a1d358b9153, 0x96541a23ccd3920f, 0x5fc91f990afe4691, + 0xd092e11f0b646037, 0x45a3e82e6476e038, +]; +const ADD_0_2: [u64; 6] = [ + 0x961a31d64d063d51, 0xaa0814ce594c2d4e, 0xe7cc77ff95d63c50, 0x4d6be1147861aa20, + 0xf573f781b4794f73, 0xe84e2aaac8bd7773, +]; +const SUB_0_2: [u64; 6] = [ + 0x61c78c3db1257187, 0x4147ea91dfdc685f, 0xcbb691edeeb52511, 0x9ae8baf41b83ece9, + 0x8dedfaed296f43e6, 0x86eddc92777c6503, +]; +const MUL_0_2: [u64; 6] = [ + 0x0d1b9a600995e9f3, 0x2f955992a1ce1f1d, 0x3e9fc8f13dde2aa6, 0xf2b8d9a4a01b31f9, + 0xb5209e68ff2733ef, 0xd4a55d775b411e88, +]; +const ADD_0_3: [u64; 6] = [ + 0xba37f6be68de8a45, 0x30a07f8d9e2003da, 0x678d6a766565b6c7, 0x36fc76f6c1feb621, + 0x2e280e6edd7f92f8, 0xa48728465622541d, +]; +const SUB_0_3: [u64; 6] = [ + 0x3da9c755954d2493, 0xbaaf7fd29b0891d3, 0x4bf59f771f25aa9a, 0xb1582511d1e6e0e9, + 0x5539e40000690061, 0xcab4def6ea17885a, +]; +const MUL_0_3: [u64; 6] = [ + 0x620fac86075a7a88, 0x55db54965c181011, 0xdf5e8a59eb274264, 0x251a6622f5d8493c, + 0x5ab9e1da86a7a8c0, 0x11f5abc43555170c, +]; +const ADD_1_2: [u64; 6] = [ + 0x1f1187d8e90b658c, 0x37cc1c250d79d29f, 0x7bedb3c113f8da17, 0xea5d91a558f7c582, + 0xd808a2884278462b, 0xb2dc78805b8edca0, +]; +const SUB_1_2: [u64; 6] = [ + 0xeabee2404d2a99c2, 0xcf0bf1e8940a0daf, 0x5fd7cdaf6cd7c2d7, 0x37da6b84fc1a084b, + 0x7082a5f3b76e3a9f, 0x517c2a680a4dca30, +]; +const MUL_1_2: [u64; 6] = [ + 0x2f38a5f5405b8ced, 0x40c58768a17f4bb4, 0xb44f790257ca1450, 0x44a9e0c5370e1c23, + 0x1a8eeee6730680cc, 0x6ead85cd3d95d42b, +]; +const ADD_1_3: [u64; 6] = [ + 0x432f4cc104e3b280, 0xbe6486e4524da92b, 0xfbaea637e388548d, 0xd3ee2787a294d182, + 0x10bcb9756b7e89b0, 0x6f15761be8f3b94a, +]; +const SUB_1_3: [u64; 6] = [ + 0xc6a11d5831524cce, 0x487387294f363723, 0xe016db389d484861, 0x4e49d5a2b27cfc4a, + 0x37ce8f068e67f71a, 0x95432ccc7ce8ed87, +]; +const MUL_1_3: [u64; 6] = [ + 0xf756beef5623ee24, 0x0f1ac48fff29cbd6, 0x4a9c68fb8c339e00, 0x9c9ecd3db790c64a, + 0x5389750f737674d2, 0x4c9c4ded9b18f3c9, +]; +const ADD_2_3: [u64; 6] = [ + 0x58706a80b7b918be, 0xef5894fbbe439b7b, 0x9bd6d88876b091b5, 0x9c13bc02a67ac937, + 0xa03a1381b4104f11, 0x1d994bb3dea5ef19, +]; +const SUB_2_3: [u64; 6] = [ + 0xdbe23b17e427b30c, 0x79679540bb2c2973, 0x803f0d8930708589, 0x166f6a1db662f3ff, + 0xc74be912d6f9bc7b, 0x43c70264729b2356, +]; +const MUL_2_3: [u64; 6] = [ + 0xa502008901e5e6c3, 0x9f4e9af2f530b046, 0xbc62f2e34e01af89, 0x2a93834573a04635, + 0x9b858a58443f35cc, 0x1fe759c6c93919a2, +]; + +const NEG_0: [u64; 6] = [ + 0x040f20f700ea2893, 0x0a58004ee36bb529, 0xa63e7b093dba4f4e, 0x0bd5b1fbb60d347a, + 0x3e4f06c8910bb653, 0x4861fc615fe311c4, +]; +const INV_0: [u64; 6] = [ + 0xdba830d9f72f1cd5, 0x7b8f2a45d29e6a64, 0x4c4b4f9d0fe87dfd, 0x0ad1ae03e3cf2e45, + 0x57a92ed46d39d475, 0x68cc63583d1a78f2, +]; +const NEG_1: [u64; 6] = [ + 0x7b17caf464e50058, 0x7c93f8f82f3e0fd8, 0x121d3f47bf97b187, 0x6ee4016ad5771919, + 0x5bba5bc2030cbf9a, 0x7dd3ae8bcd11ac97, +]; +const INV_1: [u64; 6] = [ + 0x1a054c214a83dd3c, 0xd666438735d7798f, 0x9b135a077beff004, 0xdb361fcd82e9f885, + 0xf081149c04286642, 0x5ff1a9098e3b2b6c, +]; +const NEG_2: [u64; 6] = [ + 0x65d6ad34b20f9a1a, 0x4b9feae0c3481d88, 0x71f50cf72c6f745f, 0xa6be6cefd1912164, + 0xcc3d01b5ba7afa39, 0xcf4fd8f3d75f76c7, +]; +const INV_2: [u64; 6] = [ + 0x10fd2156bb82cfc3, 0xd9255d4452084a1f, 0x6fcf54d367e4d052, 0x868b25caead5e3d6, + 0x322b760d45242df8, 0x4bcdfb88c7b2435a, +]; +const NEG_3: [u64; 6] = [ + 0x41b8e84b96374d27, 0xc50780227e7446fc, 0xf2341a805cdff9e9, 0xbd2dd70d87f41563, + 0x9388eac89174b6b4, 0x1316db5849fa9a1e, +]; +const INV_3: [u64; 6] = [ + 0xc166cb7ee36d7f0f, 0xfe7c840e3c545a8c, 0xa2a453ea31eefce0, 0x30ae03920d8a5dbe, + 0x414e93c1b106b959, 0x299cda10b995f6db, +]; + +fn fe(limbs: [u64; 6]) -> P384FieldElement { + P384FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3].map(fe); + let pairs: [(usize, usize, [u64; 6], [u64; 6], [u64; 6]); 6] = [ + (0, 1, ADD_0_1, SUB_0_1, MUL_0_1), + (0, 2, ADD_0_2, SUB_0_2, MUL_0_2), + (0, 3, ADD_0_3, SUB_0_3, MUL_0_3), + (1, 2, ADD_1_2, SUB_1_2, MUL_1_2), + (1, 3, ADD_1_3, SUB_1_3, MUL_1_3), + (2, 3, ADD_2_3, SUB_2_3, MUL_2_3), + ]; + for (i, j, a, s, m) in pairs { + assert_eq!(vals[i].add(&vals[j]), fe(a), "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), fe(a), "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), fe(s), "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), fe(m), "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), fe(m), "mul is commutative ({j},{i})"); + } + assert_eq!(vals[0].add(&P384FieldElement::ZERO), vals[0]); + assert_eq!(vals[0].mul(&P384FieldElement::ZERO), P384FieldElement::ZERO); +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3]; + let negs = [NEG_0, NEG_1, NEG_2, NEG_3]; + let invs = [INV_0, INV_1, INV_2, INV_3]; + for i in 0..4 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } + assert_eq!(P384FieldElement::ZERO.negate(), P384FieldElement::ZERO); + assert_eq!(P384FieldElement::ZERO.invert(), P384FieldElement::ZERO); +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4].map(fe); + for &v in &vals { + assert_eq!(v.add(&P384FieldElement::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&P384FieldElement::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), P384FieldElement::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), P384FieldElement::ZERO, "x + (-x) == 0"); + if v != P384FieldElement::ZERO { + assert_eq!(v.mul(&v.invert()), P384FieldElement::ONE, "x * x^-1 == 1"); + } else { + assert_eq!(v.invert(), P384FieldElement::ZERO, "0^-1 == 0 by convention"); + } + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + assert_eq!(fe(bouncycastle_ec::p384::P_LIMBS), P384FieldElement::ZERO); + let p_plus_1: [u64; 6] = [ + 0x0000000100000000, 0xffffffff00000000, 0xfffffffffffffffe, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, + ]; + assert_eq!(fe(p_plus_1), P384FieldElement::ONE); + + // 2^384 - 1 (all-ones limb pattern) reduces to C - 1 (see the module's reduction constant). + let all_ones = [u64::MAX; 6]; + let expected_c_minus_1 = + fe([0xffffffff00000000, 0x00000000ffffffff, 0x0000000000000001, 0, 0, 0]); + assert_eq!(fe(all_ones), expected_c_minus_1); +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0), "equal values must compare equal"); + for limb_idx in 0..6 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4]; + for i in 0..vals.len() { + for j in (i + 1)..vals.len() { + assert_ne!(fe(vals[i]), fe(vals[j]), "VALS_{i} must differ from VALS_{j}"); + } + } +} + +/// xorshift64* PRNG, fixed seed: same rationale as the P-256 field test's property check. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 6] { + [ + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + ] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0x243F6A8885A308D3); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != P384FieldElement::ZERO { + assert_eq!(a.mul(&a.invert()), P384FieldElement::ONE, "a * a^-1 == 1"); + } + } +} + +/// Products chosen to drive `reduce`'s seven-limb accumulator's top limb across every value it +/// can actually take (3 through 7 here; the bound proved in `reduce`'s own doc comment is 11), +/// so the top-limb fold and the final conditional subtraction are both exercised at their +/// extremes rather than only in the middle of their range. Ordinary pseudorandom operands almost +/// never reach the ends: these were found by searching ~1,000,000 products, including ones biased +/// towards all-ones high words. Expected values are Python's `(a * b) % p`, computed independently +/// of this crate's arithmetic. +/// +/// This replaces the in-module `reduce_handles_the_rare_post_two_fold_extra_bit` unit test, which +/// pinned a failure mode of the previous iterated-fold reduction (a rare carry above `2^384` after +/// exactly two folds). That code path no longer exists, and that test's input -- a raw 12-limb +/// value close to `2^768` -- is outside the `t < p^2` precondition the current `reduce` documents, +/// so it could not be carried over unchanged. +#[test] +fn known_answer_mul_at_the_reduction_bounds() { + let cases: [([u64; 6], [u64; 6], [u64; 6]); 5] = [ + // top limb 3 + ( + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ], + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ], + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ], + ), + // top limb 4 + ( + [ + 0x00000000fffffffe, 0xffffffff00000000, 0xfffffffffffffffe, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, + ], + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ], + [ + 0x00000000fffffffe, 0xffffffff00000000, 0xfffffffffffffffe, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, + ], + ), + // top limb 5 + ( + [ + 0x00000000fffffffe, 0xffffffff00000000, 0xfffffffffffffffe, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, + ], + [ + 0x00000000fffffffe, 0xffffffff00000000, 0xfffffffffffffffe, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, + ], + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ], + ), + // top limb 6 + ( + [ + 0xbacbeddb7eea0596, 0x7e8ea0660052d037, 0xf7aaf673fb46bcf4, 0x87098a5d9fa8e4f9, + 0xca88ea5329935120, 0xc366b9e6cd83ddc6, + ], + [ + 0x580a12619c5075c3, 0x384a158341144e13, 0x6a422b3b33a87fe8, 0xd825ddd3778819a8, + 0x0ac1c96633471bc1, 0x80848cd3620a3f11, + ], + [ + 0x808d58feda4b3613, 0x92636cea1ec2f04f, 0x32474225dd9fdf1e, 0x91bc9b99dc03254c, + 0xc64619e6770862bd, 0x19536cdb1e70ac25, + ], + ), + // top limb 7 + ( + [ + 0x20ed94753ac37e4e, 0x3124891b543422ac, 0xc79f52d2c078b659, 0xa41639f70ec6663a, + 0xe64693456b9add20, 0xc729a2c644808f93, + ], + [ + 0x0d25af289450acfa, 0xa3e7f461767ab80d, 0x944c547f00d76385, 0xa40ce832090ac314, + 0x259aa3cdc6083ce9, 0xb7f1365bbe074ed1, + ], + [ + 0xbb546ac08a86eb7d, 0xe1fae828114a744d, 0xb407f44e30b9bad9, 0x697806c9c6e74985, + 0xd370fd0722d05bd6, 0x29040a4e5bde6cb3, + ], + ), + ]; + for (a, b, expected) in cases { + assert_eq!(fe(a).mul(&fe(b)), fe(expected), "a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(b).mul(&fe(a)), fe(expected), "commuted: a = {a:x?}, b = {b:x?}"); + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE0000000005); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 6], + { + let mut l = [0u64; 6]; + l[0] = 1; + l + }, + [u64::MAX; 6], + { + let mut l = [u64::MAX; 6]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `is_zero` is the mask every exceptional-case select in this crate's point arithmetic keys +/// off (infinity detection, the `H == 0` / `R == 0` same-point and opposite-point cases), and +/// otherwise exercised only through those selects. Both truth values are pinned here, with +/// the nonzero side walked across every limb position so a mask that inspected only some limbs +/// would be caught. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(P384FieldElement::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::p384::P_LIMBS).is_zero().to_bool(), "p reduces to 0"); + assert!(!P384FieldElement::ONE.is_zero().to_bool()); + for limb_idx in 0..6 { + let mut limbs = [0u64; 6]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `add`'s carry-out correction and `sub`'s borrow correction, pinned at the operands that force +/// them: `(p-1) + (p-1)` is the largest sum two canonical elements can form, and `0 - 1` the +/// smallest difference. The expected values are `p - 2` and `p - 1` themselves (computed from +/// `p` in Python), so this is a check of the wrap-around handling, not of the digits of `p`. +#[test] +fn known_answer_add_carry_and_sub_borrow_at_the_extremes() { + let p_minus_1 = fe([ + 0x00000000fffffffe, 0xffffffff00000000, 0xfffffffffffffffe, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, + ]); + let p_minus_2 = fe([ + 0x00000000fffffffd, 0xffffffff00000000, 0xfffffffffffffffe, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, + ]); + let two = fe([ + 0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, + ]); + + assert_eq!(p_minus_1.add(&p_minus_1), p_minus_2, "(p-1) + (p-1) == p-2"); + assert_eq!(p_minus_1.add(&P384FieldElement::ONE), P384FieldElement::ZERO, "(p-1) + 1 == 0"); + assert_eq!(P384FieldElement::ZERO.sub(&P384FieldElement::ONE), p_minus_1, "0 - 1 == p-1"); + assert_eq!(P384FieldElement::ONE.sub(&p_minus_1), two, "1 - (p-1) == 2"); + assert_eq!(p_minus_1.negate(), P384FieldElement::ONE, "-(p-1) == 1"); + assert_eq!(P384FieldElement::ONE.invert(), P384FieldElement::ONE, "1^-1 == 1"); + assert_eq!(p_minus_1.invert(), p_minus_1, "(p-1)^-1 == p-1, since (p-1)^2 == 1"); +} diff --git a/crypto/ec/tests/p384_mul_tests.rs b/crypto/ec/tests/p384_mul_tests.rs new file mode 100644 index 000000000..ae8889936 --- /dev/null +++ b/crypto/ec/tests/p384_mul_tests.rs @@ -0,0 +1,169 @@ +//! Behavioural tests for [`multiply_point`] (`[k]Q`, constant time in `k`): cross-checked against +//! the fixed-base comb for `Q = G`, against the variable-time wNAF multiplier for a random `Q`, +//! and against `[a k mod n]G` for `Q = [a]G` (three multipliers of independent design computing +//! the same point), plus the edge cases a windowed multiplier can get wrong: `k = 0`, `k = 1`, +//! `k = n-1`, a window of all ones, `Q` the identity, `Q` negated, and `Q` given in a +//! non-canonical-`Z` Jacobian representation. + +use bouncycastle_ec::nat; +use bouncycastle_ec::p384::P384FieldElement; +use bouncycastle_ec::p384_comb::comb_multiply_base_point; +use bouncycastle_ec::p384_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p384_mul::multiply_point; +use bouncycastle_ec::p384_point::P384JacobianPoint; +use bouncycastle_ec::p384_scalar::{N_LIMBS, P384PublicScalar, P384Scalar, P384ScalarField}; +use bouncycastle_ec::p384_wnaf::shamir_multiply; + +const LIMBS: usize = 6; + +fn g() -> P384JacobianPoint { + P384JacobianPoint::from_affine( + P384FieldElement::from_limbs(G_X_LIMBS), + P384FieldElement::from_limbs(G_Y_LIMBS), + ) +} + +fn zero() -> [u64; LIMBS] { + [0; LIMBS] +} + +fn small(v: u64) -> [u64; LIMBS] { + let mut limbs = zero(); + limbs[0] = v; + limbs +} + +fn one() -> [u64; LIMBS] { + small(1) +} + +fn n_minus_1() -> [u64; LIMBS] { + let (limbs, borrow) = nat::sub(&N_LIMBS, &one()); + assert_eq!(borrow, 0); + limbs +} + +/// A pseudorandom value reduced into `[0, n)`, so it is a valid scalar for every multiplier. +fn reduced(limbs: [u64; LIMBS]) -> [u64; LIMBS] { + P384ScalarField::from_limbs(limbs).to_limbs() +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; LIMBS] { + let mut limbs = zero(); + for limb in limbs.iter_mut() { + *limb = self.next_u64(); + } + limbs + } +} + +#[test] +fn k_times_g_agrees_with_the_comb_multiplier() { + let mut rng = Xorshift64(0x9E3779B97F4A7C15); + let mut cases = vec![one(), small(2), small(3), small(15), small(16), small(17)]; + cases.push(n_minus_1()); + let mut n_minus_2 = n_minus_1(); + n_minus_2[0] -= 1; + cases.push(n_minus_2); + for _ in 0..50 { + cases.push(reduced(rng.next_limbs())); + } + for k in cases { + let via_mul = multiply_point(&P384Scalar::from_limbs(k), &g()); + let via_comb = comb_multiply_base_point(&P384Scalar::from_limbs(k)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "k = {k:x?}"); + } +} + +#[test] +fn k_times_q_agrees_with_the_wnaf_multiplier_and_with_the_scalar_field() { + let mut rng = Xorshift64(0xC2B2AE3D27D4EB4F); + for _ in 0..50 { + let a = reduced(rng.next_limbs()); + let k = reduced(rng.next_limbs()); + let q = comb_multiply_base_point(&P384Scalar::from_limbs(a)); + + let via_mul = multiply_point(&P384Scalar::from_limbs(k), &q); + + // [0]G + [k]Q by the variable-time multiplier. + let via_wnaf = shamir_multiply( + &P384PublicScalar::from_limbs(zero()), + &P384PublicScalar::from_limbs(k), + &q, + ); + assert_eq!(via_mul.to_affine(), via_wnaf.to_affine(), "wnaf: a = {a:x?}, k = {k:x?}"); + + // [a k mod n]G by the fixed-base multiplier. + let ak = P384ScalarField::from_limbs(a).mul(&P384ScalarField::from_limbs(k)).to_limbs(); + let via_comb = comb_multiply_base_point(&P384Scalar::from_limbs(ak)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "comb: a = {a:x?}, k = {k:x?}"); + } +} + +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let q = g().double(); + assert!(multiply_point(&P384Scalar::from_limbs(zero()), &q).is_infinity().to_bool()); +} + +#[test] +fn edge_case_k_equals_one_is_q() { + let q = g().double(); + assert_eq!(multiply_point(&P384Scalar::from_limbs(one()), &q).to_affine(), q.to_affine()); +} + +#[test] +fn edge_case_k_equals_n_minus_1_is_minus_q() { + let q = g().double().add(&g()); + let result = multiply_point(&P384Scalar::from_limbs(n_minus_1()), &q); + assert_eq!(result.to_affine(), q.negate().to_affine()); +} + +#[test] +fn edge_case_q_is_infinity() { + let k = reduced(Xorshift64(0x2545F4914F6CDD1D).next_limbs()); + assert!( + multiply_point(&P384Scalar::from_limbs(k), &P384JacobianPoint::INFINITY) + .is_infinity() + .to_bool() + ); +} + +#[test] +fn negating_q_negates_the_result() { + let k = reduced(Xorshift64(0x1234_5678_9ABC_DEF1).next_limbs()); + let q = g().double(); + let kq = multiply_point(&P384Scalar::from_limbs(k), &q); + let k_neg_q = multiply_point(&P384Scalar::from_limbs(k), &q.negate()); + assert_eq!(k_neg_q.to_affine(), kq.negate().to_affine()); +} + +#[test] +fn q_in_a_non_canonical_z_representation_gives_the_same_result() { + // (X, Y, Z) and (λ²X, λ³Y, λZ) are the same affine point; the multiplier must treat them alike. + let k = reduced(Xorshift64(0xDEADBEEFCAFEF00D).next_limbs()); + let q = g().double(); + let lambda = P384FieldElement::from_limbs(small(7)); + let lambda2 = lambda.mul(&lambda); + let lambda3 = lambda2.mul(&lambda); + let scaled = + P384JacobianPoint { x: q.x.mul(&lambda2), y: q.y.mul(&lambda3), z: q.z.mul(&lambda) }; + assert_eq!(scaled.to_affine(), q.to_affine(), "the scaled point is the same point"); + assert_eq!( + multiply_point(&P384Scalar::from_limbs(k), &scaled).to_affine(), + multiply_point(&P384Scalar::from_limbs(k), &q).to_affine() + ); +} diff --git a/crypto/ec/tests/p384_point_tests.rs b/crypto/ec/tests/p384_point_tests.rs new file mode 100644 index 000000000..3421341f2 --- /dev/null +++ b/crypto/ec/tests/p384_point_tests.rs @@ -0,0 +1,114 @@ +//! Known-answer tests for [`P384JacobianPoint`] arithmetic. +//! +//! `G`, `2G`, `3G` and a pseudorandom `kG` were computed independently in Python via the affine +//! group law (`a = -3`), not from this crate, and cross-checked against `n*G == infinity`: +//! +//! ```text +//! p = 2**384 - 2**128 - 2**96 + 2**32 - 1 +//! # affine add/double/scalar-mul via the standard a = -3 formulas +//! ``` + +use bouncycastle_ec::p384::P384FieldElement; +use bouncycastle_ec::p384_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p384_point::P384JacobianPoint; + +const TWO_G_X: [u64; 6] = [ + 0x5b96a9c75295df61, 0x4fe0e86ebe0e64f8, 0x51d207d19fb96e9e, 0x89025959a6f434d6, + 0x69260045c55b97f0, 0x08d999057ba3d2d9, +]; +const TWO_G_Y: [u64; 6] = [ + 0x61501e700a940e80, 0x5ffd43e94d39e22d, 0x904e505f256ab425, 0xb275d875bc6cc43e, + 0xb7bfe8dffd6dba74, 0x8e80f1fa5b1b3ced, +]; +const THREE_G_X: [u64; 6] = [ + 0x02d7e5c70500c831, 0xb408bbae5026580d, 0xbea4f240d3566da6, 0xcb9d3910202dcd06, + 0x64793c7e5fdc7d98, 0x077a41d4606ffa14, +]; +const THREE_G_Y: [u64; 6] = [ + 0xb65f28600a2f1df1, 0xc24abd6be4b5d298, 0xf7684c0edc111eac, 0x8520b41c85115aa5, + 0x7d0bbe9602a9fc99, 0xc995f7ca0b0c4283, +]; + +fn fe(limbs: [u64; 6]) -> P384FieldElement { + P384FieldElement::from_limbs(limbs) +} + +fn g() -> P384JacobianPoint { + P384JacobianPoint::from_affine(fe(G_X_LIMBS), fe(G_Y_LIMBS)) +} + +#[test] +fn known_answer_double() { + let doubled = g().double(); + let (x, y) = doubled.to_affine().unwrap(); + assert_eq!(x, fe(TWO_G_X)); + assert_eq!(y, fe(TWO_G_Y)); +} + +#[test] +fn known_answer_add() { + let sum = g().add(&g().double()); + let (x, y) = sum.to_affine().unwrap(); + assert_eq!(x, fe(THREE_G_X)); + assert_eq!(y, fe(THREE_G_Y)); +} + +#[test] +fn point_plus_negation_is_infinity() { + let g = g(); + assert!(g.add(&g.negate()).is_infinity().to_bool()); +} + +#[test] +fn infinity_identities() { + let g = g(); + let infinity = P384JacobianPoint::INFINITY; + assert!(infinity.is_infinity().to_bool()); + assert!(infinity.to_affine().is_none()); + + let (x1, y1) = g.add(&infinity).to_affine().unwrap(); + let (x2, y2) = g.to_affine().unwrap(); + assert_eq!((x1, y1), (x2, y2), "P + infinity == P"); + + let (x3, y3) = infinity.add(&g).to_affine().unwrap(); + assert_eq!((x3, y3), (x2, y2), "infinity + P == P"); + + assert!(infinity.add(&infinity).is_infinity().to_bool()); +} + +#[test] +fn scaled_z_representation_still_adds_correctly() { + // (X, Y, Z) and (X*c^2, Y*c^3, Z*c) represent the same affine point for any nonzero c; the + // branch-free add/double formulas must agree regardless of which representative is used. + let c = fe([7, 0, 0, 0, 0, 0]); + let c2 = c.mul(&c); + let c3 = c2.mul(&c); + let g = g(); + let scaled = P384JacobianPoint { x: g.x.mul(&c2), y: g.y.mul(&c3), z: g.z.mul(&c) }; + + let canonical_double = g.double().to_affine().unwrap(); + let scaled_double = scaled.double().to_affine().unwrap(); + assert_eq!(canonical_double, scaled_double); + + let canonical_sum = g.add(&g.double()).to_affine().unwrap(); + let scaled_sum = scaled.add(&g.double()).to_affine().unwrap(); + assert_eq!(canonical_sum, scaled_sum); +} + +/// `is_infinity` was previously only ever asserted true. The false side is pinned on the base +/// point and on values derived from it, and doubling the identity must stay at the identity +/// (`double` is total, per its docs, and the comb multiplier's accumulator starts there). +#[test] +fn is_infinity_is_false_for_finite_points_and_infinity_doubles_to_itself() { + let g = g(); + assert!(!g.is_infinity().to_bool(), "G is not the identity"); + assert!(!g.double().is_infinity().to_bool(), "2G is not the identity"); + assert!(!g.negate().is_infinity().to_bool(), "-G is not the identity"); + assert!(!g.add(&g.double()).is_infinity().to_bool(), "3G is not the identity"); + + let inf = P384JacobianPoint::INFINITY; + assert!(inf.double().is_infinity().to_bool(), "2 * infinity == infinity"); + assert!(inf.double().to_affine().is_none()); + assert!(inf.negate().is_infinity().to_bool(), "-infinity == infinity"); + assert!(inf.double().add(&g).to_affine().is_some(), "2 * infinity + G == G, a finite point"); +} diff --git a/crypto/ec/tests/p384_scalar_tests.rs b/crypto/ec/tests/p384_scalar_tests.rs new file mode 100644 index 000000000..e76da46a0 --- /dev/null +++ b/crypto/ec/tests/p384_scalar_tests.rs @@ -0,0 +1,380 @@ +//! Known-answer tests for [`P384ScalarField`]/[`P384Scalar`]/[`P384PublicScalar`] arithmetic mod +//! `n`. Expected values computed independently in Python: +//! +//! ```text +//! n = 0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973 +//! random.seed(9384) +//! vals = [random.randrange(1, n) for _ in range(3)] +//! # for each pair (i, j) with i < j: (vals[i]+vals[j]) % n, (vals[i]-vals[j]) % n, (vals[i]*vals[j]) % n +//! # for each i: (-vals[i]) % n, pow(vals[i], n-2, n) +//! ``` + +use bouncycastle_ec::p384_scalar::{N_LIMBS, P384PublicScalar, P384Scalar, P384ScalarField}; + +const VALS_0: [u64; 6] = [ + 0xc77cb04de57e1b92, 0xb62c49f8ac3809de, 0x87c7a84903ab1b99, 0x68a2bfa3a261cea1, + 0x83e1fdb85f5da002, 0x9ebadd01adfd8d5f, +]; +const VALS_1: [u64; 6] = [ + 0x87f663fdd6bd5cd5, 0x2909e1888cbdf5e2, 0xbc34c1e9d91e04d6, 0x992805652379593a, + 0x2ef83809b8317ce8, 0x26afaaf741ce6162, +]; +const VALS_2: [u64; 6] = [ + 0x71e0e730aa68c466, 0x3b1c409aa40050d2, 0xdb1057a1a9aa7f4e, 0x6ae61f716faca098, + 0xcc1d72f963e6eee4, 0x6dbdf6f9b2684c20, +]; + +const ADD_0_1: [u64; 6] = [ + 0x4f73144bbc3b7867, 0xdf362b8138f5ffc1, 0x43fc6a32dcc9206f, 0x01cac508c5db27dc, + 0xb2da35c2178f1ceb, 0xc56a87f8efcbeec1, +]; +const SUB_0_1: [u64; 6] = [ + 0x3f864c500ec0bebd, 0x8d2268701f7a13fc, 0xcb92e65f2a8d16c3, 0xcf7aba3e7ee87566, + 0x54e9c5aea72c2319, 0x780b320a6c2f2bfd, +]; +const MUL_0_1: [u64; 6] = [ + 0xdb53314a0c454697, 0x49175dd618eb8014, 0x63ee05b4dad0f31a, 0x38556f1911a87392, + 0x6f4c50aff5b83e01, 0xcbf09493939b36b3, +]; +const ADD_0_2: [u64; 6] = [ + 0x4c717e13c321b685, 0x992e7ce10787b336, 0x9b74b268b91e6d08, 0xd388df15120e6f3a, + 0x4fff70b1c3448ee6, 0x0c78d3fb6065d980, +]; +const SUB_0_2: [u64; 6] = [ + 0x559bc91d3b15572c, 0x7b10095e0837b90c, 0xacb750a75a009c4b, 0xfdbca03232b52e08, + 0xb7c48abefb76b11d, 0x30fce607fb95413e, +]; +const MUL_0_2: [u64; 6] = [ + 0x05cecf9406d1d6fa, 0xd342b24814030464, 0xb7a6b1f2265e9292, 0x9e77bf497f796523, + 0x193b612f3a494ef5, 0xfac75e0481d64319, +]; +const ADD_1_2: [u64; 6] = [ + 0xf9d74b2e8126213b, 0x6426222330be46b4, 0x9745198b82c88424, 0x040e24d69325f9d3, + 0xfb15ab031c186bcd, 0x946da1f0f436ad82, +]; +const SUB_1_2: [u64; 6] = [ + 0x03019637f919c1e2, 0x4607aea0316e4c8b, 0xa887b7ca23aab367, 0x2e41e5f3b3ccb8a1, + 0x62dac510544a8e04, 0xb8f1b3fd8f661541, +]; +const MUL_1_2: [u64; 6] = [ + 0xbb833806cd4db543, 0x241e4163ff6bf90f, 0x70892959a4f05521, 0xcd9551642d7a908c, + 0x5e99c1b61df8c0c6, 0xf7419ab4682320ae, +]; + +const NEG_0: [u64; 6] = [ + 0x256f691ce7470de1, 0xa1edc3b99c789d9c, 0x3f9ba538f08c1245, 0x975d405c5d9e315e, + 0x7c1e0247a0a25ffd, 0x614522fe520272a0, +]; +const INV_0: [u64; 6] = [ + 0xb1f58808d22a26b3, 0xffca32fa8e1d9879, 0x0a44a0ec41e0054a, 0x4ede6c11a032a009, + 0x0d1688b0b0ebbde3, 0x53fda3065b0e5295, +]; +const NEG_1: [u64; 6] = [ + 0x64f5b56cf607cc9e, 0x2f102c29bbf2b198, 0x0b2e8b981b192909, 0x66d7fa9adc86a6c5, + 0xd107c7f647ce8317, 0xd9505508be319e9d, +]; +const INV_1: [u64; 6] = [ + 0x45fc1aeb26427fab, 0xbf3d4a75c92d09c8, 0xffc73cb11601001a, 0x63b7c81239d64bb3, + 0xdfb85303caeafa88, 0x1a3e4c9e01391dec, +]; +const NEG_2: [u64; 6] = [ + 0x7b0b323a225c650d, 0x1cfdcd17a4b056a8, 0xec52f5e04a8cae91, 0x9519e08e90535f66, + 0x33e28d069c19111b, 0x924209064d97b3df, +]; +const INV_2: [u64; 6] = [ + 0x7e91a30141508a8f, 0xdcd67e7cf7cb26ec, 0xe038bda04dbbbfd5, 0x207ef90b210da851, + 0xcd01c5c997c5371d, 0x2df22a92c65648b0, +]; + +fn fe(limbs: [u64; 6]) -> P384ScalarField { + P384ScalarField::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + let pairs: [(usize, usize, [u64; 6], [u64; 6], [u64; 6]); 3] = [ + (0, 1, ADD_0_1, SUB_0_1, MUL_0_1), + (0, 2, ADD_0_2, SUB_0_2, MUL_0_2), + (1, 2, ADD_1_2, SUB_1_2, MUL_1_2), + ]; + for (i, j, a, s, m) in pairs { + assert_eq!(vals[i].add(&vals[j]), fe(a), "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), fe(a), "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), fe(s), "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), fe(m), "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), fe(m), "mul is commutative ({j},{i})"); + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2]; + let negs = [NEG_0, NEG_1, NEG_2]; + let invs = [INV_0, INV_1, INV_2]; + for i in 0..3 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + for &v in &vals { + assert_eq!(v.add(&P384ScalarField::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&P384ScalarField::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), P384ScalarField::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), P384ScalarField::ZERO, "x + (-x) == 0"); + assert_eq!(v.mul(&v.invert()), P384ScalarField::ONE, "x * x^-1 == 1"); + } + assert_eq!(P384ScalarField::ZERO.invert(), P384ScalarField::ZERO, "0^-1 == 0 by convention"); +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..6 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + assert_eq!(fe(N_LIMBS), P384ScalarField::ZERO); +} + +#[test] +fn secret_scalar_reduces_and_round_trips() { + let secret = P384Scalar::from_limbs(VALS_0); + assert_eq!(P384ScalarField::from_secret(&secret), fe(VALS_0)); +} + +#[test] +fn secret_scalar_reduces_out_of_range_input() { + let secret = P384Scalar::from_limbs(N_LIMBS); + assert_eq!(P384ScalarField::from_secret(&secret), P384ScalarField::ZERO); +} + +#[test] +fn secret_scalar_be_bytes_round_trip() { + for limbs in [VALS_0, VALS_1, VALS_2] { + let secret = P384Scalar::from_limbs(limbs); + let round_tripped = P384Scalar::from_be_bytes(&secret.to_be_bytes()); + assert_eq!(round_tripped, secret); + } +} + +#[test] +fn public_scalar_round_trips_and_reduces() { + for limbs in [VALS_0, VALS_1, VALS_2] { + assert_eq!(P384PublicScalar::from_limbs(limbs).to_limbs(), limbs); + } + assert_eq!(P384PublicScalar::from_limbs(N_LIMBS).to_limbs(), [0; 6]); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the P-256 field test's property check. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 6] { + [ + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + ] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0xBB67AE8584CAA73B); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != P384ScalarField::ZERO { + assert_eq!(a.mul(&a.invert()), P384ScalarField::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE000000000D); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 6], + { + let mut l = [0u64; 6]; + l[0] = 1; + l + }, + [u64::MAX; 6], + { + let mut l = [u64::MAX; 6]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `zeroize` is the signing path's way of not leaving `d`, `k` or `k^-1` legible on the stack +/// (see the method's own docs for why this type is not wrapped in `Secret` instead). Whether the +/// volatile write survives optimization cannot be observed from Rust; that the value is actually +/// cleared can be, and is what would break if the method were ever reduced to a no-op. +#[test] +fn zeroize_clears_the_value() { + let mut a = fe([1, 2, 3, 4, 5, 6]); + assert_ne!(a, P384ScalarField::ZERO, "precondition: the value starts non-zero"); + a.zeroize(); + assert_eq!(a, P384ScalarField::ZERO, "zeroize must leave the value at zero"); + + // and it is idempotent, so a caller scrubbing twice on overlapping paths is harmless + a.zeroize(); + assert_eq!(a, P384ScalarField::ZERO); +} + +/// `is_zero` on the scalar field, pinned for both truth values and across every limb position +/// (see the field test of the same name for why); it is the check behind `negate`'s `0 -> 0` +/// special case. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(P384ScalarField::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::p384_scalar::N_LIMBS).is_zero().to_bool(), "n reduces to 0"); + assert!(!P384ScalarField::ONE.is_zero().to_bool()); + for limb_idx in 0..6 { + let mut limbs = [0u64; 6]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `n - 1`, the largest canonical scalar, is the worst case for REDC's final conditional +/// subtraction and for `add`'s carry correction, and was absent from every known-answer set +/// above (whose values are pseudorandom, so never near `n`). Expected values follow from +/// `(n-1)^2 == 1` and `(n-1) + (n-1) == n - 2` in the field, computed from `n` in Python. +#[test] +fn known_answer_at_n_minus_1() { + let n_minus_1_limbs: [u64; 6] = [ + 0xecec196accc52972, 0x581a0db248b0a77a, 0xc7634d81f4372ddf, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, + ]; + let n_minus_1 = fe(n_minus_1_limbs); + let n_minus_2 = fe([ + 0xecec196accc52971, 0x581a0db248b0a77a, 0xc7634d81f4372ddf, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, + ]); + + assert_eq!(n_minus_1.to_limbs(), n_minus_1_limbs, "Montgomery round trip of n-1"); + assert_eq!(P384PublicScalar::from_limbs(n_minus_1_limbs).to_limbs(), n_minus_1_limbs); + assert_eq!(n_minus_1.add(&n_minus_1), n_minus_2, "(n-1) + (n-1) == n-2"); + assert_eq!(n_minus_1.add(&P384ScalarField::ONE), P384ScalarField::ZERO, "(n-1) + 1 == 0"); + assert_eq!(P384ScalarField::ZERO.sub(&P384ScalarField::ONE), n_minus_1, "0 - 1 == n-1"); + assert_eq!(n_minus_1.mul(&n_minus_1), P384ScalarField::ONE, "(n-1)^2 == 1"); + assert_eq!(n_minus_1.square(), P384ScalarField::ONE, "(n-1)^2 == 1, via square"); + assert_eq!(n_minus_1.negate(), P384ScalarField::ONE, "-(n-1) == 1"); + assert_eq!(n_minus_1.invert(), n_minus_1, "(n-1)^-1 == n-1"); + assert_eq!(P384ScalarField::ONE.invert(), P384ScalarField::ONE, "1^-1 == 1"); +} + +/// The variable-time inverse on the public scalar type must agree with the constant-time +/// [`P384ScalarField::invert`] everywhere: on the known-answer values (whose inverses are pinned above +/// from Python), on `0` (both return `0` by convention), `1`, `n - 1`, and on pseudorandom +/// values -- including ones with long runs of zero bits, which are where the binary algorithm +/// takes its longest halving chains. +#[test] +fn public_scalar_invert_vartime_agrees_with_constant_time_invert() { + let n_minus_1 = { + let mut l = bouncycastle_ec::p384_scalar::N_LIMBS; + l[0] -= 1; + l + }; + let mut one = [0u64; 6]; + one[0] = 1; + // Values whose low limb is exactly 1 with higher limbs set: odd, so the binary algorithm must + // not halve them, yet indistinguishable from `1` by their low limb alone. A halving test that + // looked at more than the lowest bit (or at the wrong limb) breaks the `u == x1 * a` invariant + // on exactly these and nowhere the pseudorandom inputs below would reach. + let mut low_limb_one_a = [0u64; 6]; + low_limb_one_a[0] = 1; + low_limb_one_a[1] = 1; + let mut low_limb_one_b = [0u64; 6]; + low_limb_one_b[0] = 1; + low_limb_one_b[5] = 1; + let mut low_limb_one_c = [0u64; 6]; + low_limb_one_c[0] = 1; + low_limb_one_c[1] = u64::MAX; + let mut fixed: Vec<[u64; 6]> = + vec![[0u64; 6], one, n_minus_1, low_limb_one_a, low_limb_one_b, low_limb_one_c]; + fixed.extend([VALS_0, VALS_1, VALS_2]); + for limbs in fixed { + assert_eq!( + bouncycastle_ec::p384_scalar::P384PublicScalar::from_limbs(limbs).invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } + assert_eq!( + bouncycastle_ec::p384_scalar::P384PublicScalar::from_limbs([0u64; 6]).invert_vartime(), + P384ScalarField::ZERO, + "0^-1 == 0 by convention, matching invert" + ); + + let mut rng = Xorshift64(0x1AC0_B5EC_0000_0006); + for i in 0..500 { + let mut limbs = rng.next_limbs(); + if i % 4 == 0 { + // Sparse values: clear the low half of each limb so `u`/`v` shed many bits per step. + for limb in limbs.iter_mut() { + *limb &= 0xffff_ffff_0000_0000; + } + } + assert_eq!( + bouncycastle_ec::p384_scalar::P384PublicScalar::from_limbs(limbs).invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } +} diff --git a/crypto/ec/tests/p384_sec1_tests.rs b/crypto/ec/tests/p384_sec1_tests.rs new file mode 100644 index 000000000..9efce0797 --- /dev/null +++ b/crypto/ec/tests/p384_sec1_tests.rs @@ -0,0 +1,135 @@ +//! Known-answer tests for SEC 1 §2.3.3/§2.3.4 point encoding and decoding, P-384. +//! +//! `G` and `2G`'s coordinates are the same values verified independently in +//! `p384_point_tests.rs`; the encodings below were derived from those coordinates independently +//! in Python via plain big-endian byte packing. `G` has odd `y` (tag `0x03`) and `2G` has even `y` +//! (tag `0x02`), so between them both compressed tags are exercised. + +use bouncycastle_ec::p384::P384FieldElement; +use bouncycastle_ec::p384_sec1::{decode, decode_point, encode_compressed, encode_uncompressed}; + +const G_X: [u64; 6] = [ + 0x3a545e3872760ab7, 0x5502f25dbf55296c, 0x59f741e082542a38, 0x6e1d3b628ba79b98, + 0x8eb1c71ef320ad74, 0xaa87ca22be8b0537, +]; +const G_Y: [u64; 6] = [ + 0x7a431d7c90ea0e5f, 0x0a60b1ce1d7e819d, 0xe9da3113b5f0b8c0, 0xf8f41dbd289a147c, + 0x5d9e98bf9292dc29, 0x3617de4a96262c6f, +]; +const TWO_G_X: [u64; 6] = [ + 0x5b96a9c75295df61, 0x4fe0e86ebe0e64f8, 0x51d207d19fb96e9e, 0x89025959a6f434d6, + 0x69260045c55b97f0, 0x08d999057ba3d2d9, +]; +const TWO_G_Y: [u64; 6] = [ + 0x61501e700a940e80, 0x5ffd43e94d39e22d, 0x904e505f256ab425, 0xb275d875bc6cc43e, + 0xb7bfe8dffd6dba74, 0x8e80f1fa5b1b3ced, +]; + +const UNCOMPRESSED_G: [u8; 97] = [ + 0x04, 0xaa, 0x87, 0xca, 0x22, 0xbe, 0x8b, 0x05, 0x37, 0x8e, 0xb1, 0xc7, 0x1e, 0xf3, 0x20, 0xad, + 0x74, 0x6e, 0x1d, 0x3b, 0x62, 0x8b, 0xa7, 0x9b, 0x98, 0x59, 0xf7, 0x41, 0xe0, 0x82, 0x54, 0x2a, + 0x38, 0x55, 0x02, 0xf2, 0x5d, 0xbf, 0x55, 0x29, 0x6c, 0x3a, 0x54, 0x5e, 0x38, 0x72, 0x76, 0x0a, + 0xb7, 0x36, 0x17, 0xde, 0x4a, 0x96, 0x26, 0x2c, 0x6f, 0x5d, 0x9e, 0x98, 0xbf, 0x92, 0x92, 0xdc, + 0x29, 0xf8, 0xf4, 0x1d, 0xbd, 0x28, 0x9a, 0x14, 0x7c, 0xe9, 0xda, 0x31, 0x13, 0xb5, 0xf0, 0xb8, + 0xc0, 0x0a, 0x60, 0xb1, 0xce, 0x1d, 0x7e, 0x81, 0x9d, 0x7a, 0x43, 0x1d, 0x7c, 0x90, 0xea, 0x0e, + 0x5f, +]; + +const COMPRESSED_G: [u8; 49] = [ + 0x03, 0xaa, 0x87, 0xca, 0x22, 0xbe, 0x8b, 0x05, 0x37, 0x8e, 0xb1, 0xc7, 0x1e, 0xf3, 0x20, 0xad, + 0x74, 0x6e, 0x1d, 0x3b, 0x62, 0x8b, 0xa7, 0x9b, 0x98, 0x59, 0xf7, 0x41, 0xe0, 0x82, 0x54, 0x2a, + 0x38, 0x55, 0x02, 0xf2, 0x5d, 0xbf, 0x55, 0x29, 0x6c, 0x3a, 0x54, 0x5e, 0x38, 0x72, 0x76, 0x0a, + 0xb7, +]; + +const COMPRESSED_2G: [u8; 49] = [ + 0x02, 0x08, 0xd9, 0x99, 0x05, 0x7b, 0xa3, 0xd2, 0xd9, 0x69, 0x26, 0x00, 0x45, 0xc5, 0x5b, 0x97, + 0xf0, 0x89, 0x02, 0x59, 0x59, 0xa6, 0xf4, 0x34, 0xd6, 0x51, 0xd2, 0x07, 0xd1, 0x9f, 0xb9, 0x6e, + 0x9e, 0x4f, 0xe0, 0xe8, 0x6e, 0xbe, 0x0e, 0x64, 0xf8, 0x5b, 0x96, 0xa9, 0xc7, 0x52, 0x95, 0xdf, + 0x61, +]; + +fn fe(limbs: [u64; 6]) -> P384FieldElement { + P384FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_encode_uncompressed() { + assert_eq!(encode_uncompressed(&fe(G_X), &fe(G_Y)), UNCOMPRESSED_G); +} + +#[test] +fn known_answer_encode_compressed() { + assert_eq!(encode_compressed(&fe(G_X), &fe(G_Y)), COMPRESSED_G, "G has odd y -> tag 0x03"); + assert_eq!( + encode_compressed(&fe(TWO_G_X), &fe(TWO_G_Y)), + COMPRESSED_2G, + "2G has even y -> tag 0x02" + ); +} + +#[test] +fn known_answer_decode_uncompressed() { + let (x, y) = decode(&UNCOMPRESSED_G).expect("valid uncompressed point"); + assert_eq!(x, fe(G_X)); + assert_eq!(y, fe(G_Y)); +} + +#[test] +fn known_answer_decode_compressed() { + let (x, y) = decode(&COMPRESSED_G).expect("valid compressed point, tag 0x03"); + assert_eq!(x, fe(G_X)); + assert_eq!(y, fe(G_Y)); + + let (x2, y2) = decode(&COMPRESSED_2G).expect("valid compressed point, tag 0x02"); + assert_eq!(x2, fe(TWO_G_X)); + assert_eq!(y2, fe(TWO_G_Y)); +} + +#[test] +fn round_trip_uncompressed_and_compressed() { + for (x, y) in [(G_X, G_Y), (TWO_G_X, TWO_G_Y)] { + let uncompressed = encode_uncompressed(&fe(x), &fe(y)); + assert_eq!(decode(&uncompressed), Some((fe(x), fe(y)))); + + let compressed = encode_compressed(&fe(x), &fe(y)); + assert_eq!(decode(&compressed), Some((fe(x), fe(y)))); + + assert!(decode_point(&uncompressed).unwrap().to_affine().is_some()); + } +} + +#[test] +fn decode_rejects_malformed_input() { + assert_eq!(decode(&[]), None); + assert_eq!(decode(&[0x04; 96]), None); + assert_eq!(decode(&[0x04; 98]), None); + assert_eq!(decode(&[0x02; 32]), None); + + let mut bad_tag_97 = UNCOMPRESSED_G; + bad_tag_97[0] = 0x02; + assert_eq!(decode(&bad_tag_97), None); + + let mut bad_tag_49 = COMPRESSED_G; + bad_tag_49[0] = 0x04; + assert_eq!(decode(&bad_tag_49), None); + + assert_eq!(decode(&[0x00]), None); + + let mut x_too_big = COMPRESSED_G; + x_too_big[1..49].fill(0xff); + assert_eq!(decode(&x_too_big), None); + + let mut no_root = COMPRESSED_G; + no_root[1..49].fill(0); + no_root[48] = 4; // x = 4, confirmed (in the Python derivation) to have no square root + assert_eq!(decode(&no_root), None); + + let mut not_on_curve = UNCOMPRESSED_G; + not_on_curve[96] ^= 0x01; + assert_eq!(decode(¬_on_curve), None); + + let mut y_too_big = UNCOMPRESSED_G; + y_too_big[49..97].fill(0xff); + assert_eq!(decode(&y_too_big), None); +} diff --git a/crypto/ec/tests/p384_wnaf_tests.rs b/crypto/ec/tests/p384_wnaf_tests.rs new file mode 100644 index 000000000..0e1704f7f --- /dev/null +++ b/crypto/ec/tests/p384_wnaf_tests.rs @@ -0,0 +1,180 @@ +//! Known-answer tests for [`shamir_multiply`] (`[u]G + [v]Q`), P-384. +//! +//! `Q` below is `d*G` for a fixed test-only `d` (not a real key). Expected results were computed +//! independently in Python from the affine group law, the same way as `p384_comb_tests.rs`, with +//! `random.seed(808384)`. + +use bouncycastle_ec::p384::P384FieldElement; +use bouncycastle_ec::p384_comb::comb_multiply_base_point; +use bouncycastle_ec::p384_point::P384JacobianPoint; +use bouncycastle_ec::p384_scalar::{P384PublicScalar, P384Scalar}; +use bouncycastle_ec::p384_wnaf::shamir_multiply; + +const Q_X: [u64; 6] = [ + 0x4f6d6d13beeaa1b2, 0x3cb1efd1edd7bbff, 0xe07a2b63cdb9d941, 0xefec1918a1158b0a, + 0x354ecd94081fe303, 0xaacd2057b4583a67, +]; +const Q_Y: [u64; 6] = [ + 0x5e371d1feb74472f, 0x9a9227ba8e5ef0e2, 0x6db85468ef6a5e50, 0xe369fa64fc786ffb, + 0x9a4d67905a6d9075, 0x3f61f852d831009a, +]; + +fn q() -> P384JacobianPoint { + P384JacobianPoint::from_affine( + P384FieldElement::from_limbs(Q_X), + P384FieldElement::from_limbs(Q_Y), + ) +} + +fn scalar(limbs: [u64; 6]) -> P384PublicScalar { + P384PublicScalar::from_limbs(limbs) +} + +fn assert_result_is(u: [u64; 6], v: [u64; 6], expected_x: [u64; 6], expected_y: [u64; 6]) { + let result = shamir_multiply(&scalar(u), &scalar(v), &q()); + let (rx, ry) = result.to_affine().expect("expected a non-infinity result"); + assert_eq!(rx, P384FieldElement::from_limbs(expected_x), "x mismatch for u={u:x?} v={v:x?}"); + assert_eq!(ry, P384FieldElement::from_limbs(expected_y), "y mismatch for u={u:x?} v={v:x?}"); +} + +#[test] +fn known_answer_large_u_v() { + let u: [u64; 6] = [ + 0x6149a1f0473fdc0a, 0xd13f8f211ee42ca3, 0x798bbf759162536a, 0xb4981a8fbcfde9fc, + 0xc8c7279f85b5b57d, 0xdf4dc003c482b49d, + ]; + let v: [u64; 6] = [ + 0xcf9d0a68d7fd9587, 0x72c8c95608f4eb96, 0x48403aad19a04ae6, 0x74cc2df91a4a3ac6, + 0xa73e462c3bc5bf0c, 0x838e541242a28502, + ]; + assert_result_is( + u, + v, + [ + 0x709d13c4c9203cb0, 0x134fb91a2120df5f, 0x263d03fac7eb441c, 0xcd2bdbabfd72bf84, + 0x982474564005e793, 0x39882178259925d2, + ], + [ + 0x2809b7a310810609, 0x8d963924b0abffb4, 0xc256c6703ce538b4, 0x0f262b8555a127b9, + 0x401c3b38d2a6ea23, 0xf6eb81915b6b13ac, + ], + ); +} + +#[test] +fn edge_case_v_is_zero_reduces_to_u_times_g() { + assert_result_is( + [5, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [ + 0x0abcdbc3836d84bc, 0x37882f4a1ca297e6, 0x4f6661cbe56583b0, 0xf208e51dbff98fc5, + 0x573cac5ea025e467, 0x11de24a2c251c777, + ], + [ + 0x184414abe6c1713a, 0x3177686d0ae8fb33, 0x8c986533b6901aeb, 0x284b447754d5dee8, + 0x0f5837e90a00e7c5, 0x8fa696c77440f92d, + ], + ); +} + +#[test] +fn edge_case_u_is_zero_reduces_to_v_times_q() { + assert_result_is( + [0, 0, 0, 0, 0, 0], + [5, 0, 0, 0, 0, 0], + [ + 0xf931c4adf0579053, 0xdabd5bd1fbde76cb, 0x171924159a115ea8, 0x7caf68c567529296, + 0x894cecf0212acf00, 0x014bdb66a196ff9e, + ], + [ + 0xa8d76dc9a8914d0c, 0x6cbfa095ab83f08f, 0xa47412ad0e695029, 0xb5a7b2caee0f8b05, + 0x8991406ffb6c6039, 0x14dfd74b3c35d557, + ], + ); +} + +#[test] +fn edge_case_both_zero_is_infinity() { + let result = shamir_multiply(&scalar([0; 6]), &scalar([0; 6]), &q()); + assert!(result.to_affine().is_none()); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 6] { + [ + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + self.next_u64(), + ] + } +} + +#[test] +fn cross_checked_against_the_comb_multiplier_over_many_pseudorandom_scalars() { + // shamir_multiply(k, 0, anything) must equal comb_multiply_base_point(k): two independently + // implemented multipliers (fixed-base comb vs. interleaved wNAF) computing the same [k]G. + let mut rng = Xorshift64(0xE7C558A6A6E7F1DE); + for _ in 0..500 { + let k_limbs = rng.next_limbs(); + let via_comb = comb_multiply_base_point(&P384Scalar::from_limbs(k_limbs)); + let via_wnaf = + shamir_multiply(&P384PublicScalar::from_limbs(k_limbs), &scalar([0; 6]), &q()); + assert_eq!(via_comb.to_affine(), via_wnaf.to_affine(), "k = {k_limbs:x?}"); + } +} + +/// Every other test in this file uses one fixed `Q = dG`. The three `Q` values here each take a +/// path that one cannot: `Q = infinity` makes every `[v]Q` digit an addition of the identity, +/// `Q = G` makes the two precomputed odd-multiple tables identical (so the interleaved +/// additions hit the same-point case of `add_vartime`), and `Q = -G` makes them negatives of +/// each other (the opposite-point case, and with `u == v` a final result of infinity). Expected +/// values come from the fixed-base comb multiplier on the combined scalar, which is pinned by +/// its own known-answer tests. +#[test] +fn edge_case_q_is_infinity_g_or_minus_g() { + let g = P384JacobianPoint::from_affine( + P384FieldElement::from_limbs(bouncycastle_ec::p384_domain::G_X_LIMBS), + P384FieldElement::from_limbs(bouncycastle_ec::p384_domain::G_Y_LIMBS), + ); + let mut u = [0u64; 6]; + u[0] = 12345; + let mut v = [0u64; 6]; + v[0] = 6789; + let mut u_plus_v = [0u64; 6]; + u_plus_v[0] = 19134; + let mut u_minus_v = [0u64; 6]; + u_minus_v[0] = 5556; + let via_comb = |k: [u64; 6]| comb_multiply_base_point(&P384Scalar::from_limbs(k)).to_affine(); + + // [u]G + [v]infinity == [u]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &P384JacobianPoint::INFINITY).to_affine(), + via_comb(u) + ); + // [u]G + [v]G == [u+v]G + assert_eq!(shamir_multiply(&scalar(u), &scalar(v), &g).to_affine(), via_comb(u_plus_v)); + // [u]G + [v](-G) == [u-v]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &g.negate()).to_affine(), + via_comb(u_minus_v) + ); + // [u]G + [u](-G) == infinity + assert!(shamir_multiply(&scalar(u), &scalar(u), &g.negate()).is_infinity().to_bool()); + // [0]G + [v]G == [v]G: the G table is never consulted, only Q's + assert_eq!(shamir_multiply(&scalar([0u64; 6]), &scalar(v), &g).to_affine(), via_comb(v)); +} diff --git a/crypto/ec/tests/p521_comb_tests.rs b/crypto/ec/tests/p521_comb_tests.rs new file mode 100644 index 000000000..e0b249fc3 --- /dev/null +++ b/crypto/ec/tests/p521_comb_tests.rs @@ -0,0 +1,87 @@ +//! Known-answer tests for [`comb_multiply_base_point`] (`[k]G`), P-521. + +use bouncycastle_ec::p521::P521FieldElement; +use bouncycastle_ec::p521_comb::comb_multiply_base_point; +use bouncycastle_ec::p521_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p521_scalar::P521Scalar; + +fn scalar(limbs: [u64; 9]) -> P521Scalar { + P521Scalar::from_limbs(limbs) +} + +fn assert_result_is(k_limbs: [u64; 9], x: [u64; 9], y: [u64; 9]) { + let result = comb_multiply_base_point(&scalar(k_limbs)); + let (rx, ry) = result.to_affine().expect("[k]G must not be infinity for k in (0, n)"); + assert_eq!(rx, P521FieldElement::from_limbs(x), "x mismatch for k = {k_limbs:x?}"); + assert_eq!(ry, P521FieldElement::from_limbs(y), "y mismatch for k = {k_limbs:x?}"); +} + +#[test] +fn known_answer_small_k() { + assert_result_is([1, 0, 0, 0, 0, 0, 0, 0, 0], G_X_LIMBS, G_Y_LIMBS); + assert_result_is( + [5, 0, 0, 0, 0, 0, 0, 0, 0], + [ + 0xd5ab5096ec8f3078, 0x29d7e1e6d8931738, 0x7112feaf137e79a3, 0x383c0c6d5e301423, + 0xcf03dab8f177ace4, 0x7a596efdb53f0d24, 0x3dbc3391c04eb0bf, 0x2bf3c52927a432c7, + 0x0000000000000065, + ], + [ + 0x173cc3e8deb090cb, 0xd1f007257354f7f8, 0x311540211cf5ff79, 0xbb6897c9072cf374, + 0xedd817c9a0347087, 0x1cd8fe8e872e0051, 0x8a2b73114a811291, 0xe6ef1bdd6601d6ec, + 0x000000000000015b, + ], + ); +} + +#[test] +fn known_answer_large_k() { + let k: [u64; 9] = [ + 0x567890abcdef1234, 0x567890abcdef1234, 0x567890abcdef1234, 0x567890abcdef1234, + 0x567890abcdef1234, 0x567890abcdef1234, 0x0000000000001234, 0x0000000000000000, + 0x0000000000000000, + ]; + assert_result_is( + k, + [ + 0x9c699e0dc9fc64bd, 0x71fd91794bcdc2a0, 0xc5931b2b3c897eca, 0x6a436b057927cd65, + 0x134afc788e3091d1, 0x184c257a06780b62, 0x89e928cbc24609e1, 0x1d7e631e94d5d55e, + 0x0000000000000090, + ], + [ + 0x16e3badd0f622dd6, 0xc4bdc69316f08a60, 0xe3e41939aef7be70, 0x7f7b5988fddfab94, + 0xdc1bd8be391d5c9e, 0xc81a987e9e63dbda, 0x71184edd42174cd9, 0x66263adc88c73be8, + 0x000000000000017a, + ], + ); +} + +#[test] +fn edge_case_k_equals_n_minus_1() { + // (n-1)*G == -G: same x as G, negated y. + let n_minus_1: [u64; 9] = [ + 0xbb6fb71e91386408, 0x3bb5c9b8899c47ae, 0x7fcc0148f709a5d0, 0x51868783bf2f966b, + 0xfffffffffffffffa, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ]; + assert_result_is( + n_minus_1, + G_X_LIMBS, + [ + 0x77416b89602e99af, 0xcac38f795d8d3dbf, 0x3aaf46fec052f89e, 0x68118d66a10bd9bf, + 0xe85042e8d8c199d3, 0x670abbb6a864bb97, 0xa375a04bd382e426, 0xc6d6958765c43ffb, + 0x00000000000000e7, + ], + ); +} + +/// `k = 0` is the one scalar for which `[k]G` is the identity: every comb digit is `0`, so every +/// round selects table entry `0` (the infinity sentinel) and the accumulator never leaves +/// `INFINITY`. Nothing in the signing path can produce it (`k` is in `[1, n-1]` by construction), +/// which is exactly why it needs pinning here rather than being left to chance. +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let result = comb_multiply_base_point(&scalar([0u64; 9])); + assert!(result.is_infinity().to_bool()); + assert!(result.to_affine().is_none()); +} diff --git a/crypto/ec/tests/p521_extra_bits_tests.rs b/crypto/ec/tests/p521_extra_bits_tests.rs new file mode 100644 index 000000000..41b855cde --- /dev/null +++ b/crypto/ec/tests/p521_extra_bits_tests.rs @@ -0,0 +1,88 @@ +//! Known-answer tests for [`reduce_wide_bits_mod_n_minus_1`] (FIPS 186-5 Appendix A.4.1 steps +//! 3-5) over P-521's `n`. Expected values computed independently in Python via +//! `(int.from_bytes(bytes, 'big') % (n - 1)) + 1` with `n` from SP 800-186 §3.2.1.5, not from this +//! crate's own reduction code; this is exactly what would have caught the wide-reduction bug the +//! module's docs describe, had it existed from the start. + +use bouncycastle_ec::nat; +use bouncycastle_ec::p521_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::p521_scalar::{N_LIMBS, P521Scalar}; +use bouncycastle_ec::p521_sec1; + +/// The width `bouncycastle-ecdsa` draws for P-521: `ceil((521 + 64) / 8)`. +const DRBG_OUTPUT_LEN: usize = 74; +/// SEC 1 octet width of a P-521 scalar. +const N_LEN: usize = 66; + +/// `n - 1`, little-endian limbs. +fn n_minus_1_limbs() -> [u64; 9] { + let (limbs, borrow) = nat::sub(&N_LIMBS, &[1, 0, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!(borrow, 0); + limbs +} + +#[test] +fn known_answer_all_ff_at_the_drbg_width() { + // 74 bytes of 0xff: the width keygen and ECDSAP521 actually draw. Expected value is + // Python's ((2^592 - 1) % (n - 1)) + 1. + let input = [0xffu8; DRBG_OUTPUT_LEN]; + let expected: [u64; 9] = [ + 0x0000000000000000, 0x482470b763cdfc00, 0x251b23bb31dc28a2, 0x19ff5b847b2d17e2, + 0x3cbc3e206834ca40, 0x00000000000002d7, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, + ]; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input), P521Scalar::from_limbs(expected)); +} + +#[test] +fn known_answer_all_ff() { + let input = [0xffu8; N_LEN]; + let expected: [u64; 9] = [ + 0x482470b763cdfc00, 0x251b23bb31dc28a2, 0x19ff5b847b2d17e2, 0x3cbc3e206834ca40, + 0x00000000000002d7, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, + ]; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input), P521Scalar::from_limbs(expected)); +} + +#[test] +fn known_answer_repeating_pattern() { + let mut input = [0u8; N_LEN]; + for (i, b) in input.iter_mut().enumerate().take(64) { + *b = [0x01, 0x02, 0x03, 0x04][i % 4]; + } + input[64] = 0x05; + input[65] = 0x06; + let expected: [u64; 9] = [ + 0x0304010203040507, 0x0304010203040102, 0x0304010203040102, 0x0304010203040102, + 0x0304010203040102, 0x0304010203040102, 0x0304010203040102, 0x0304010203040102, + 0x0000000000000102, + ]; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input), P521Scalar::from_limbs(expected)); +} + +#[test] +fn known_answer_at_the_n_minus_1_boundary() { + // n - 1 is the modulus of Appendix A.4.1 step 4 itself, so it reduces to 0 and step 5 + // makes it 1; n - 2 is the largest residue and comes out as n - 1, the top of the + // output interval. + let n_minus_1 = p521_sec1::be_bytes_from_limbs(&n_minus_1_limbs()); + let mut expected_one = [0u64; 9]; + expected_one[0] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&n_minus_1), P521Scalar::from_limbs(expected_one)); + + let mut n_minus_2 = n_minus_1; + n_minus_2[N_LEN - 1] -= 1; // n - 1 is even (n is prime), so this never borrows + assert_eq!( + reduce_wide_bits_mod_n_minus_1(&n_minus_2), + P521Scalar::from_limbs(n_minus_1_limbs()) + ); +} + +#[test] +fn known_answer_all_zero_yields_one() { + let input = [0u8; N_LEN]; + let mut expected = [0u64; 9]; + expected[0] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input), P521Scalar::from_limbs(expected)); +} diff --git a/crypto/ec/tests/p521_field_tests.rs b/crypto/ec/tests/p521_field_tests.rs new file mode 100644 index 000000000..c5cf0409b --- /dev/null +++ b/crypto/ec/tests/p521_field_tests.rs @@ -0,0 +1,531 @@ +//! Known-answer tests for [`P521FieldElement`] arithmetic. +//! +//! `p = 2**521 - 1` (a Mersenne prime). Expected values computed independently in Python: +//! +//! ```text +//! p = 2**521 - 1 +//! random.seed(5521) +//! vals = [random.randrange(1, p) for _ in range(4)] + [0] +//! # for each pair (i, j) with i < j: (vals[i]+vals[j]) % p, (vals[i]-vals[j]) % p, (vals[i]*vals[j]) % p +//! # for each i: (-vals[i]) % p, pow(vals[i], p-2, p) if vals[i] != 0 else 0 +//! ``` + +use bouncycastle_ec::p521::P521FieldElement; + +const VALS_0: [u64; 9] = [ + 0xb08b02bcbe1a026f, 0x6ac488dbbc8cd11f, 0x003d56d89b9f4cbb, 0x4d4bb7d2842e9790, + 0xe86a5650cfe21007, 0x4c7042c229a92107, 0x91854abbc0616220, 0x873c6380b019c44f, + 0x0000000000000051, +]; +const VALS_1: [u64; 9] = [ + 0xed0a9b9aba01374f, 0x26374507740013c3, 0xdd87874ca2b46fb6, 0x22904689b624e3b5, + 0xca4770c12693afe9, 0x66fad105f7a891d9, 0xa51f452115f34715, 0x6eb27e737ad931f1, + 0x000000000000003b, +]; +const VALS_2: [u64; 9] = [ + 0x67b414d048d25b17, 0x311e0def6fc4e352, 0x779d713c52df9b16, 0xc947965c490081e6, + 0xf7036f209bcd40b9, 0x324fad803dbd6a0a, 0xa1980b35c1a2a7b0, 0x324eee29a82f54b1, + 0x00000000000000e6, +]; +const VALS_3: [u64; 9] = [ + 0x323abbc2772f3a85, 0x43e1ef8055199654, 0x2869ed6f16b488d7, 0x8ac5ece9e24296a5, + 0xceb010a6fdfe5509, 0x24bf4fe166fb3034, 0x31817bc54c7ce6e2, 0xcdbbe03f68211317, + 0x0000000000000001, +]; +const VALS_4: [u64; 9] = [0, 0, 0, 0, 0, 0, 0, 0, 0]; + +const ADD_0_1: [u64; 9] = [ + 0x9d959e57781b39be, 0x90fbcde3308ce4e3, 0xddc4de253e53bc71, 0x6fdbfe5c3a537b45, + 0xb2b1c711f675bff0, 0xb36b13c82151b2e1, 0x36a48fdcd654a935, 0xf5eee1f42af2f641, + 0x000000000000008c, +]; +const SUB_0_1: [u64; 9] = [ + 0xc38067220418cb20, 0x448d43d4488cbd5b, 0x22b5cf8bf8eadd05, 0x2abb7148ce09b3da, + 0x1e22e58fa94e601e, 0xe57571bc32008f2e, 0xec66059aaa6e1b0a, 0x1889e50d3540925d, + 0x0000000000000016, +]; +const MUL_0_1: [u64; 9] = [ + 0x751afa5884d561d7, 0xe24d4d2f56f4c78a, 0xd1cd1c910b34e1b3, 0x1350bc38b5e023ce, + 0xa56bee5956ba51cf, 0xb39085c080f2c977, 0xf4dbcacd56201d75, 0x1f72ac36f2e48919, + 0x00000000000001d6, +]; +const ADD_0_2: [u64; 9] = [ + 0x183f178d06ec5d86, 0x9be296cb2c51b472, 0x77dac814ee7ee7d1, 0x16934e2ecd2f1976, + 0xdf6dc5716baf50c1, 0x7ebff04267668b12, 0x331d55f1820409d0, 0xb98b51aa58491901, + 0x0000000000000137, +]; +const SUB_0_2: [u64; 9] = [ + 0x48d6edec7547a757, 0x39a67aec4cc7edcd, 0x889fe59c48bfb1a5, 0x840421763b2e15a9, + 0xf166e7303414cf4d, 0x1a209541ebebb6fc, 0xefed3f85febeba70, 0x54ed755707ea6f9d, + 0x000000000000016b, +]; +const MUL_0_2: [u64; 9] = [ + 0x974b28754c14ae4c, 0x515e779315bd3f66, 0x432a322bfba84065, 0xd8d191310203997c, + 0xfb67175b539a5027, 0x4304dc1fb6b6ae93, 0x6724999eacc6ce8a, 0xd5b4ca2dc78b2653, + 0x0000000000000126, +]; +const ADD_0_3: [u64; 9] = [ + 0xe2c5be7f35493cf4, 0xaea6785c11a66773, 0x28a74447b253d592, 0xd811a4bc66712e35, + 0xb71a66f7cde06510, 0x712f92a390a4513c, 0xc306c6810cde4902, 0x54f843c0183ad766, + 0x0000000000000053, +]; +const SUB_0_3: [u64; 9] = [ + 0x7e5046fa46eac7ea, 0x26e2995b67733acb, 0xd7d3696984eac3e4, 0xc285cae8a1ec00ea, + 0x19ba45a9d1e3bafd, 0x27b0f2e0c2adf0d3, 0x6003cef673e47b3e, 0xb980834147f8b138, + 0x000000000000004f, +]; +const MUL_0_3: [u64; 9] = [ + 0x5295972e7ad4d804, 0xdfed3e64b6ce929f, 0x9ad22d437defbe30, 0x521340e6494bd3fe, + 0x029163ba5c7fc336, 0xd3d01c6dfb8df628, 0x883a1e7e946d9b55, 0xdd6972560ebfdafd, + 0x0000000000000199, +]; +const ADD_1_2: [u64; 9] = [ + 0x54beb06b02d39266, 0x575552f6e3c4f716, 0x5524f888f5940acc, 0xebd7dce5ff25659c, + 0xc14adfe1c260f0a2, 0x994a7e863565fbe4, 0x46b75056d795eec5, 0xa1016c9d230886a3, + 0x0000000000000121, +]; +const SUB_1_2: [u64; 9] = [ + 0x855686ca712edc37, 0xf5193718043b3071, 0x65ea16104fd4d49f, 0x5948b02d6d2461cf, + 0xd34401a08ac66f2f, 0x34ab2385b9eb27ce, 0x038739eb54509f65, 0x3c639049d2a9dd40, + 0x0000000000000155, +]; +const MUL_1_2: [u64; 9] = [ + 0xf23936889b0d7b35, 0x7e7fc75399fc5955, 0xf3c1cad740900842, 0x280ecdb8d49bb407, + 0x2033cd0cb97bd350, 0x63421fdca07c0475, 0xb5b19d6eec34dbc1, 0xdb7970cb51e54504, + 0x0000000000000029, +]; +const ADD_1_3: [u64; 9] = [ + 0x1f45575d313071d4, 0x6a193487c919aa18, 0x05f174bbb968f88d, 0xad56337398677a5b, + 0x98f78168249204f2, 0x8bba20e75ea3c20e, 0xd6a0c0e662702df7, 0x3c6e5eb2e2fa4508, + 0x000000000000003d, +]; +const SUB_1_3: [u64; 9] = [ + 0xbacfdfd842d1fcca, 0xe25555871ee67d6f, 0xb51d99dd8bffe6de, 0x97ca599fd3e24d10, + 0xfb97601a28955adf, 0x423b812490ad61a4, 0x739dc95bc9766033, 0xa0f69e3412b81eda, + 0x0000000000000039, +]; +const MUL_1_3: [u64; 9] = [ + 0xe0542c0bef81d68f, 0x9aabd78758e554a6, 0x89161b86be49be6f, 0x70b058b93b29953b, + 0x0ccdedc02b7622a3, 0x001097362623fc85, 0x82ab612506649a2c, 0x6eeddc31e40b1401, + 0x000000000000007a, +]; +const ADD_2_3: [u64; 9] = [ + 0x99eed092c001959c, 0x74fffd6fc4de79a6, 0xa0075eab699423ed, 0x540d83462b43188b, + 0xc5b37fc799cb95c3, 0x570efd61a4b89a3f, 0xd31986fb0e1f8e92, 0x000ace69105067c8, + 0x00000000000000e8, +]; +const SUB_2_3: [u64; 9] = [ + 0x3579590dd1a32092, 0xed3c1e6f1aab4cfe, 0x4f3383cd3c2b123e, 0x3e81a97266bdeb41, + 0x28535e799dceebb0, 0x0d905d9ed6c239d6, 0x70168f707525c0ce, 0x64930dea400e419a, + 0x00000000000000e4, +]; +const MUL_2_3: [u64; 9] = [ + 0x75131f7a45b9df1d, 0x935d6745d9ad16a0, 0x3a001f01b7fbd984, 0x6e5a0ebbc32d649f, + 0x5fac2ec6ffeceba1, 0x69bdddce48d4f3ae, 0xf8a3dba6043225a3, 0x934cecb643aa2a76, + 0x0000000000000048, +]; + +const NEG_0: [u64; 9] = [ + 0x4f74fd4341e5fd90, 0x953b772443732ee0, 0xffc2a9276460b344, 0xb2b4482d7bd1686f, + 0x1795a9af301deff8, 0xb38fbd3dd656def8, 0x6e7ab5443f9e9ddf, 0x78c39c7f4fe63bb0, + 0x00000000000001ae, +]; +const INV_0: [u64; 9] = [ + 0x765019b3c416cef7, 0x937d091f336dbc98, 0x20610e6c269802ba, 0xbe8b78b421aa0f14, + 0xf428758f04375867, 0xe16a40104d9f0ca9, 0x75441de4778d185f, 0x0266bbcffa3397cf, + 0x0000000000000141, +]; +const NEG_1: [u64; 9] = [ + 0x12f5646545fec8b0, 0xd9c8baf88bffec3c, 0x227878b35d4b9049, 0xdd6fb97649db1c4a, + 0x35b88f3ed96c5016, 0x99052efa08576e26, 0x5ae0badeea0cb8ea, 0x914d818c8526ce0e, + 0x00000000000001c4, +]; +const INV_1: [u64; 9] = [ + 0xa9f7fdcc883d790a, 0x31dbfd751c4250f4, 0x0dfab71235deec59, 0x5c58a0d138d1d243, + 0x29f6cc72712e8c26, 0x712f07fc72de2218, 0x37a8288272a69641, 0x125028819eb3cf56, + 0x00000000000000d9, +]; +const NEG_2: [u64; 9] = [ + 0x984beb2fb72da4e8, 0xcee1f210903b1cad, 0x88628ec3ad2064e9, 0x36b869a3b6ff7e19, + 0x08fc90df6432bf46, 0xcdb0527fc24295f5, 0x5e67f4ca3e5d584f, 0xcdb111d657d0ab4e, + 0x0000000000000119, +]; +const INV_2: [u64; 9] = [ + 0x57373e572a80f5e9, 0x25eeb110d1fbe14a, 0x3524058a18e31a27, 0x22446ea205766379, + 0x22f1b87b4f317fb7, 0x6f76c35e81fd26fe, 0x88a2f7a3ce574c38, 0xa7799b1684813bca, + 0x0000000000000173, +]; +const NEG_3: [u64; 9] = [ + 0xcdc5443d88d0c57a, 0xbc1e107faae669ab, 0xd7961290e94b7728, 0x753a13161dbd695a, + 0x314fef590201aaf6, 0xdb40b01e9904cfcb, 0xce7e843ab383191d, 0x32441fc097deece8, + 0x00000000000001fe, +]; +const INV_3: [u64; 9] = [ + 0x1c82f93cb5d23d5a, 0x9dc8d98efc5b6c5c, 0x6aef6188770fabf2, 0x34f04ad0725ce874, + 0x0980ed475850ba6a, 0xc2ee047d658d2a3e, 0x3d20e88bbd169a35, 0x0fa60699685d41bb, + 0x00000000000000dc, +]; + +fn fe(limbs: [u64; 9]) -> P521FieldElement { + P521FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3].map(fe); + let pairs: [(usize, usize, [u64; 9], [u64; 9], [u64; 9]); 6] = [ + (0, 1, ADD_0_1, SUB_0_1, MUL_0_1), + (0, 2, ADD_0_2, SUB_0_2, MUL_0_2), + (0, 3, ADD_0_3, SUB_0_3, MUL_0_3), + (1, 2, ADD_1_2, SUB_1_2, MUL_1_2), + (1, 3, ADD_1_3, SUB_1_3, MUL_1_3), + (2, 3, ADD_2_3, SUB_2_3, MUL_2_3), + ]; + for (i, j, a, s, m) in pairs { + assert_eq!(vals[i].add(&vals[j]), fe(a), "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), fe(a), "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), fe(s), "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), fe(m), "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), fe(m), "mul is commutative ({j},{i})"); + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3]; + let negs = [NEG_0, NEG_1, NEG_2, NEG_3]; + let invs = [INV_0, INV_1, INV_2, INV_3]; + for i in 0..4 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4].map(fe); + for &v in &vals { + assert_eq!(v.add(&P521FieldElement::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&P521FieldElement::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), P521FieldElement::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), P521FieldElement::ZERO, "x + (-x) == 0"); + if v != P521FieldElement::ZERO { + assert_eq!(v.mul(&v.invert()), P521FieldElement::ONE, "x * x^-1 == 1"); + } else { + assert_eq!(v.invert(), P521FieldElement::ZERO, "0^-1 == 0 by convention"); + } + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + assert_eq!(fe(bouncycastle_ec::p521::P_LIMBS), P521FieldElement::ZERO); + + // p + 1 == 2^521: sets only bit 521 (limb 8's bit 9), the smallest value this type's spare + // top-limb bits could silently lose if from_limbs simply masked them away instead of folding + // them in via 2^521 == 1 (mod p). + let p_plus_1: [u64; 9] = [0, 0, 0, 0, 0, 0, 0, 0, 0x200]; + assert_eq!(fe(p_plus_1), P521FieldElement::ONE); + + // the maximum representable 9-limb value (2^576 - 1) must also reduce correctly: it folds to + // lo (2^521 - 1, i.e. p itself) plus hi (2^55 - 1), and p + (2^55 - 1) reduces once more to + // 2^55 - 1. + let all_ones = [u64::MAX; 9]; + let expected = fe([0x007fffffffffffff, 0, 0, 0, 0, 0, 0, 0, 0]); + assert_eq!(fe(all_ones), expected); +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..9 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4]; + for i in 0..vals.len() { + for j in (i + 1)..vals.len() { + assert_ne!(fe(vals[i]), fe(vals[j]), "VALS_{i} must differ from VALS_{j}"); + } + } +} + +/// xorshift64* PRNG, fixed seed: same rationale as the P-256 field test's property check. Values +/// are masked to the top limb's 9 valid bits so every generated element is already `< 2^521`. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 9] { + let mut limbs = [0u64; 9]; + for l in limbs.iter_mut() { + *l = self.next_u64(); + } + limbs[8] &= 0x1ff; + limbs + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0x9E3779B97F4A7C15); + // 1000, not the 5000 used by the faster P-256/P-384 field tests: P-521's invert() does 521 + // repeated squarings per call (vs 256/384), and this loop calls it once per iteration, so + // 5000 iterations would make this single test dominate the crate's whole test-suite runtime + // (and, via cargo mutants, every mutant's timeout budget) for marginal extra coverage. + for _ in 0..1000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != P521FieldElement::ZERO { + assert_eq!(a.mul(&a.invert()), P521FieldElement::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE0000000008); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 9], + { + let mut l = [0u64; 9]; + l[0] = 1; + l + }, + [u64::MAX; 9], + { + let mut l = [u64::MAX; 9]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `is_zero` is the mask every exceptional-case select in this crate's point arithmetic keys +/// off (infinity detection, the `H == 0` / `R == 0` same-point and opposite-point cases), and +/// otherwise exercised only through those selects. Both truth values are pinned here, with +/// the nonzero side walked across every limb position so a mask that inspected only some limbs +/// would be caught. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(P521FieldElement::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::p521::P_LIMBS).is_zero().to_bool(), "p reduces to 0"); + assert!(!P521FieldElement::ONE.is_zero().to_bool()); + for limb_idx in 0..9 { + let mut limbs = [0u64; 9]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `add`'s carry-out correction and `sub`'s borrow correction, pinned at the operands that force +/// them: `(p-1) + (p-1)` is the largest sum two canonical elements can form, and `0 - 1` the +/// smallest difference. The expected values are `p - 2` and `p - 1` themselves (computed from +/// `p` in Python), so this is a check of the wrap-around handling, not of the digits of `p`. +#[test] +fn known_answer_add_carry_and_sub_borrow_at_the_extremes() { + let p_minus_1 = fe([ + 0xfffffffffffffffe, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ]); + let p_minus_2 = fe([ + 0xfffffffffffffffd, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ]); + let two = fe([ + 0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, + ]); + + assert_eq!(p_minus_1.add(&p_minus_1), p_minus_2, "(p-1) + (p-1) == p-2"); + assert_eq!(p_minus_1.add(&P521FieldElement::ONE), P521FieldElement::ZERO, "(p-1) + 1 == 0"); + assert_eq!(P521FieldElement::ZERO.sub(&P521FieldElement::ONE), p_minus_1, "0 - 1 == p-1"); + assert_eq!(P521FieldElement::ONE.sub(&p_minus_1), two, "1 - (p-1) == 2"); + assert_eq!(p_minus_1.negate(), P521FieldElement::ONE, "-(p-1) == 1"); + assert_eq!(P521FieldElement::ONE.invert(), P521FieldElement::ONE, "1^-1 == 1"); + assert_eq!(p_minus_1.invert(), p_minus_1, "(p-1)^-1 == p-1, since (p-1)^2 == 1"); +} + +/// Products at the reduction's extremes rather than in the middle of its range, the same class +/// of check `p256_field_tests.rs`/`p384_field_tests.rs`/`sm2_field_tests.rs` already carry and +/// this curve did not: `(p-1)^2` and `(p-1)(p-2)` (the largest products two canonical elements +/// can form), operands within `2^64` of `p`, and operands with the top bit set. Expected values +/// are Python's `(a * b) % p`, computed independently of this crate's arithmetic +/// (`random.seed(20260918)` for the pseudorandom operands). +#[test] +fn known_answer_mul_at_the_reduction_bounds() { + let cases: [([u64; 9], [u64; 9], [u64; 9]); 7] = [ + // (p-1)^2 == 1 + ( + [ + 0xfffffffffffffffe, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ], + [ + 0xfffffffffffffffe, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ], + [ + 0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, + ], + ), + // (p-1)(p-2) == 2 + ( + [ + 0xfffffffffffffffe, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ], + [ + 0xfffffffffffffffd, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ], + [ + 0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, + ], + ), + // (p-1) * 2^(bits-1) == p - 2^(bits-1) + ( + [ + 0xfffffffffffffffe, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ], + [ + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000100, + ], + [ + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000000ff, + ], + ), + // both operands within 2^64 of p + ( + [ + 0xb122a0b01e939352, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ], + [ + 0xd88def3c21b98f23, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ], + [ + 0x31b01cefe60714ac, 0x0c26dd43c9f82aba, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, + ], + ), + // both operands within 2^64 of p + ( + [ + 0xf315ee0261d4b798, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ], + [ + 0x64b1d0d0007c0705, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ], + [ + 0x9bf784140a1d7c96, 0x07d5aa976f3513e2, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, + ], + ), + // both operands with the top bit set + ( + [ + 0x6e63c6bbea7082bb, 0xd22cc93d5a21e76c, 0xa0dbe54bcdb2f7cb, 0xa8c5e4ca5347a8d3, + 0x5bad1998281ff5a4, 0xbd0325a0d3d545e4, 0x8c806bda1c0c4b43, 0x5e96526496fe66ac, + 0x000000000000011f, + ], + [ + 0x37bb2c7c190c5b04, 0xdc528dfd30b9df11, 0x561993c90be7b6f0, 0x2ea5d8b1d76b6c45, + 0xda9b6732aab17048, 0x4a1aa8b29a612687, 0x3c3dd306f7772c1c, 0xa0ca82769b2831e8, + 0x00000000000001c2, + ], + [ + 0x200dc6bca4512d5f, 0xa1d8fbb9571990d8, 0x5d1b9d6437792d0b, 0x997e20eb51eae882, + 0x3c3636ba1b5d85f8, 0x819b6bf32325eef8, 0x93dbeb22a8656147, 0xa1940a58dc0e865e, + 0x000000000000019e, + ], + ), + // both operands with the top bit set + ( + [ + 0xaf7088097f55e903, 0x8e581673473c58f9, 0xaae64e123971ea1c, 0x7e9ad2be8bd62f1b, + 0x81a2b599fe27ff63, 0x8e426b5910999ef7, 0x337e0ac8281bf0fb, 0x3ea808d1210c096c, + 0x00000000000001a5, + ], + [ + 0x28a550a89905d51d, 0x60c2abda1d6524d7, 0x6b3ff74c431643d4, 0x0f69fb5a42f1a376, + 0x4645b3fbe987802e, 0x8c1260cd82f83971, 0x2a1d57fa3de42173, 0x09a3149642f3827e, + 0x00000000000001e7, + ], + [ + 0x5ae5c8c3efee0a33, 0x9995143d8f4bd505, 0xfea8df171b8bd60f, 0x264256831b8916b6, + 0xf4868cc125af95da, 0x9e2375949e1d32ad, 0x5ab64ea04020cd71, 0x7964125674dfcc19, + 0x0000000000000036, + ], + ), + ]; + for (a, b, expected) in cases { + assert_eq!(fe(a).mul(&fe(b)), fe(expected), "a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(b).mul(&fe(a)), fe(expected), "commuted: a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(a).square(), fe(a).mul(&fe(a)), "square at the same extreme: a = {a:x?}"); + } +} diff --git a/crypto/ec/tests/p521_mul_tests.rs b/crypto/ec/tests/p521_mul_tests.rs new file mode 100644 index 000000000..85099840a --- /dev/null +++ b/crypto/ec/tests/p521_mul_tests.rs @@ -0,0 +1,169 @@ +//! Behavioural tests for [`multiply_point`] (`[k]Q`, constant time in `k`): cross-checked against +//! the fixed-base comb for `Q = G`, against the variable-time wNAF multiplier for a random `Q`, +//! and against `[a k mod n]G` for `Q = [a]G` (three multipliers of independent design computing +//! the same point), plus the edge cases a windowed multiplier can get wrong: `k = 0`, `k = 1`, +//! `k = n-1`, a window of all ones, `Q` the identity, `Q` negated, and `Q` given in a +//! non-canonical-`Z` Jacobian representation. + +use bouncycastle_ec::nat; +use bouncycastle_ec::p521::P521FieldElement; +use bouncycastle_ec::p521_comb::comb_multiply_base_point; +use bouncycastle_ec::p521_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p521_mul::multiply_point; +use bouncycastle_ec::p521_point::P521JacobianPoint; +use bouncycastle_ec::p521_scalar::{N_LIMBS, P521PublicScalar, P521Scalar, P521ScalarField}; +use bouncycastle_ec::p521_wnaf::shamir_multiply; + +const LIMBS: usize = 9; + +fn g() -> P521JacobianPoint { + P521JacobianPoint::from_affine( + P521FieldElement::from_limbs(G_X_LIMBS), + P521FieldElement::from_limbs(G_Y_LIMBS), + ) +} + +fn zero() -> [u64; LIMBS] { + [0; LIMBS] +} + +fn small(v: u64) -> [u64; LIMBS] { + let mut limbs = zero(); + limbs[0] = v; + limbs +} + +fn one() -> [u64; LIMBS] { + small(1) +} + +fn n_minus_1() -> [u64; LIMBS] { + let (limbs, borrow) = nat::sub(&N_LIMBS, &one()); + assert_eq!(borrow, 0); + limbs +} + +/// A pseudorandom value reduced into `[0, n)`, so it is a valid scalar for every multiplier. +fn reduced(limbs: [u64; LIMBS]) -> [u64; LIMBS] { + P521ScalarField::from_limbs(limbs).to_limbs() +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; LIMBS] { + let mut limbs = zero(); + for limb in limbs.iter_mut() { + *limb = self.next_u64(); + } + limbs + } +} + +#[test] +fn k_times_g_agrees_with_the_comb_multiplier() { + let mut rng = Xorshift64(0x9E3779B97F4A7C15); + let mut cases = vec![one(), small(2), small(3), small(15), small(16), small(17)]; + cases.push(n_minus_1()); + let mut n_minus_2 = n_minus_1(); + n_minus_2[0] -= 1; + cases.push(n_minus_2); + for _ in 0..50 { + cases.push(reduced(rng.next_limbs())); + } + for k in cases { + let via_mul = multiply_point(&P521Scalar::from_limbs(k), &g()); + let via_comb = comb_multiply_base_point(&P521Scalar::from_limbs(k)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "k = {k:x?}"); + } +} + +#[test] +fn k_times_q_agrees_with_the_wnaf_multiplier_and_with_the_scalar_field() { + let mut rng = Xorshift64(0xC2B2AE3D27D4EB4F); + for _ in 0..50 { + let a = reduced(rng.next_limbs()); + let k = reduced(rng.next_limbs()); + let q = comb_multiply_base_point(&P521Scalar::from_limbs(a)); + + let via_mul = multiply_point(&P521Scalar::from_limbs(k), &q); + + // [0]G + [k]Q by the variable-time multiplier. + let via_wnaf = shamir_multiply( + &P521PublicScalar::from_limbs(zero()), + &P521PublicScalar::from_limbs(k), + &q, + ); + assert_eq!(via_mul.to_affine(), via_wnaf.to_affine(), "wnaf: a = {a:x?}, k = {k:x?}"); + + // [a k mod n]G by the fixed-base multiplier. + let ak = P521ScalarField::from_limbs(a).mul(&P521ScalarField::from_limbs(k)).to_limbs(); + let via_comb = comb_multiply_base_point(&P521Scalar::from_limbs(ak)); + assert_eq!(via_mul.to_affine(), via_comb.to_affine(), "comb: a = {a:x?}, k = {k:x?}"); + } +} + +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let q = g().double(); + assert!(multiply_point(&P521Scalar::from_limbs(zero()), &q).is_infinity().to_bool()); +} + +#[test] +fn edge_case_k_equals_one_is_q() { + let q = g().double(); + assert_eq!(multiply_point(&P521Scalar::from_limbs(one()), &q).to_affine(), q.to_affine()); +} + +#[test] +fn edge_case_k_equals_n_minus_1_is_minus_q() { + let q = g().double().add(&g()); + let result = multiply_point(&P521Scalar::from_limbs(n_minus_1()), &q); + assert_eq!(result.to_affine(), q.negate().to_affine()); +} + +#[test] +fn edge_case_q_is_infinity() { + let k = reduced(Xorshift64(0x2545F4914F6CDD1D).next_limbs()); + assert!( + multiply_point(&P521Scalar::from_limbs(k), &P521JacobianPoint::INFINITY) + .is_infinity() + .to_bool() + ); +} + +#[test] +fn negating_q_negates_the_result() { + let k = reduced(Xorshift64(0x1234_5678_9ABC_DEF1).next_limbs()); + let q = g().double(); + let kq = multiply_point(&P521Scalar::from_limbs(k), &q); + let k_neg_q = multiply_point(&P521Scalar::from_limbs(k), &q.negate()); + assert_eq!(k_neg_q.to_affine(), kq.negate().to_affine()); +} + +#[test] +fn q_in_a_non_canonical_z_representation_gives_the_same_result() { + // (X, Y, Z) and (λ²X, λ³Y, λZ) are the same affine point; the multiplier must treat them alike. + let k = reduced(Xorshift64(0xDEADBEEFCAFEF00D).next_limbs()); + let q = g().double(); + let lambda = P521FieldElement::from_limbs(small(7)); + let lambda2 = lambda.mul(&lambda); + let lambda3 = lambda2.mul(&lambda); + let scaled = + P521JacobianPoint { x: q.x.mul(&lambda2), y: q.y.mul(&lambda3), z: q.z.mul(&lambda) }; + assert_eq!(scaled.to_affine(), q.to_affine(), "the scaled point is the same point"); + assert_eq!( + multiply_point(&P521Scalar::from_limbs(k), &scaled).to_affine(), + multiply_point(&P521Scalar::from_limbs(k), &q).to_affine() + ); +} diff --git a/crypto/ec/tests/p521_point_tests.rs b/crypto/ec/tests/p521_point_tests.rs new file mode 100644 index 000000000..cc10b2ccd --- /dev/null +++ b/crypto/ec/tests/p521_point_tests.rs @@ -0,0 +1,109 @@ +//! Known-answer tests for [`P521JacobianPoint`] arithmetic. `G`, `2G`, `3G` computed independently +//! in Python via the affine group law (`a = -3`), cross-checked against `n*G == infinity`. + +use bouncycastle_ec::p521::P521FieldElement; +use bouncycastle_ec::p521_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::p521_point::P521JacobianPoint; + +const TWO_G_X: [u64; 9] = [ + 0xf43e3933ba6d783d, 0xcf2fa364d60fd967, 0xaa104a3a35c5af41, 0xb3b204da6ef55507, + 0x2c6e5505d769be97, 0x7403279b1ccc0635, 0x2fcb288148c28274, 0x3c219024277e7e68, + 0x0000000000000043, +]; +const TWO_G_Y: [u64; 9] = [ + 0x1be356d661f41b02, 0xeafcbe95edc0f4f7, 0x93937fa99a3248f4, 0xb3e377de9f251f6b, + 0xab21a29906c42dbb, 0xc6b5107c4da97740, 0xa7f3eceeeed3f0b5, 0xbb8cc7f86db26700, + 0x00000000000000f4, +]; +const THREE_G_X: [u64; 9] = [ + 0xa5919d2ede37ad7d, 0xaeb490862c32ea05, 0x1da6bd16b59fe21b, 0xad3f164a3a483205, + 0xe5ad7a112d7a8dd1, 0xb52a6e5b123d9ab9, 0xd91d6a64b5959479, 0x3d352443de29195d, + 0x00000000000001a7, +]; +const THREE_G_Y: [u64; 9] = [ + 0x5f588ca1ee86c0e5, 0xf105c9bc93a59042, 0x2d5aced1dec3c70c, 0x2e2dd4cf8dc575b0, + 0xd2f8ab1fa355ceec, 0xf1557fa82a9d0317, 0x979f86c6cab814f2, 0x9b03b97dfa62ddd9, + 0x000000000000013e, +]; + +fn fe(limbs: [u64; 9]) -> P521FieldElement { + P521FieldElement::from_limbs(limbs) +} + +fn g() -> P521JacobianPoint { + P521JacobianPoint::from_affine(fe(G_X_LIMBS), fe(G_Y_LIMBS)) +} + +#[test] +fn known_answer_double() { + let doubled = g().double(); + let (x, y) = doubled.to_affine().unwrap(); + assert_eq!(x, fe(TWO_G_X)); + assert_eq!(y, fe(TWO_G_Y)); +} + +#[test] +fn known_answer_add() { + let sum = g().add(&g().double()); + let (x, y) = sum.to_affine().unwrap(); + assert_eq!(x, fe(THREE_G_X)); + assert_eq!(y, fe(THREE_G_Y)); +} + +#[test] +fn point_plus_negation_is_infinity() { + let g = g(); + assert!(g.add(&g.negate()).is_infinity().to_bool()); +} + +#[test] +fn infinity_identities() { + let g = g(); + let infinity = P521JacobianPoint::INFINITY; + assert!(infinity.is_infinity().to_bool()); + assert!(infinity.to_affine().is_none()); + + let (x1, y1) = g.add(&infinity).to_affine().unwrap(); + let (x2, y2) = g.to_affine().unwrap(); + assert_eq!((x1, y1), (x2, y2), "P + infinity == P"); + + let (x3, y3) = infinity.add(&g).to_affine().unwrap(); + assert_eq!((x3, y3), (x2, y2), "infinity + P == P"); + + assert!(infinity.add(&infinity).is_infinity().to_bool()); +} + +#[test] +fn scaled_z_representation_still_adds_correctly() { + let c = fe([7, 0, 0, 0, 0, 0, 0, 0, 0]); + let c2 = c.mul(&c); + let c3 = c2.mul(&c); + let g = g(); + let scaled = P521JacobianPoint { x: g.x.mul(&c2), y: g.y.mul(&c3), z: g.z.mul(&c) }; + + let canonical_double = g.double().to_affine().unwrap(); + let scaled_double = scaled.double().to_affine().unwrap(); + assert_eq!(canonical_double, scaled_double); + + let canonical_sum = g.add(&g.double()).to_affine().unwrap(); + let scaled_sum = scaled.add(&g.double()).to_affine().unwrap(); + assert_eq!(canonical_sum, scaled_sum); +} + +/// `is_infinity` was previously only ever asserted true. The false side is pinned on the base +/// point and on values derived from it, and doubling the identity must stay at the identity +/// (`double` is total, per its docs, and the comb multiplier's accumulator starts there). +#[test] +fn is_infinity_is_false_for_finite_points_and_infinity_doubles_to_itself() { + let g = g(); + assert!(!g.is_infinity().to_bool(), "G is not the identity"); + assert!(!g.double().is_infinity().to_bool(), "2G is not the identity"); + assert!(!g.negate().is_infinity().to_bool(), "-G is not the identity"); + assert!(!g.add(&g.double()).is_infinity().to_bool(), "3G is not the identity"); + + let inf = P521JacobianPoint::INFINITY; + assert!(inf.double().is_infinity().to_bool(), "2 * infinity == infinity"); + assert!(inf.double().to_affine().is_none()); + assert!(inf.negate().is_infinity().to_bool(), "-infinity == infinity"); + assert!(inf.double().add(&g).to_affine().is_some(), "2 * infinity + G == G, a finite point"); +} diff --git a/crypto/ec/tests/p521_scalar_tests.rs b/crypto/ec/tests/p521_scalar_tests.rs new file mode 100644 index 000000000..3ebb5c5e3 --- /dev/null +++ b/crypto/ec/tests/p521_scalar_tests.rs @@ -0,0 +1,425 @@ +//! Known-answer tests for [`P521ScalarField`]/[`P521Scalar`]/[`P521PublicScalar`] arithmetic mod +//! `n`. Expected values computed independently in Python: +//! +//! ```text +//! n = 0x1ff...386409 (SP 800-186 S3.2.1.5) +//! random.seed(5219) +//! vals = [random.randrange(1, n) for _ in range(3)] +//! # for each pair (i, j) with i < j: (vals[i]+vals[j]) % n, (vals[i]-vals[j]) % n, (vals[i]*vals[j]) % n +//! # for each i: (-vals[i]) % n, pow(vals[i], n-2, n) +//! ``` + +use bouncycastle_ec::p521_scalar::{N_LIMBS, P521PublicScalar, P521Scalar, P521ScalarField}; + +const VALS_0: [u64; 9] = [ + 0x5ce2f91065c121a3, 0xc8e8826a5dcc634c, 0x4f47b2008123a7a7, 0xab27f49f94c37c7d, + 0x4a4c1bd63979854c, 0x3ce767e9c4382eec, 0x4959fc2dbe7df0bc, 0x129a79eb6fd54403, + 0x0000000000000102, +]; +const VALS_1: [u64; 9] = [ + 0x93985d64330b16c6, 0xa23434d4e6af01f9, 0x3ff355eb83baff0e, 0xdad0d0f13b77ff4d, + 0x17ba7558683f48d6, 0x6927b5c13d54de16, 0x62f5cbef7f9256c7, 0x0912c6abda627bac, + 0x00000000000001dd, +]; +const VALS_2: [u64; 9] = [ + 0xd99593ba9e8b62bc, 0x420bf9c9cd23eda2, 0x6b277737a52c9953, 0x021f3e401f9ecc08, + 0xbcceb29f4d04f8b2, 0x77af21289d42a7cc, 0xb09286a4f455bdbd, 0xcdfc2630981136b8, + 0x00000000000000c0, +]; + +const ADD_0_1: [u64; 9] = [ + 0x350b9f560793d460, 0x2f66ed86badf1d97, 0x0f6f06a30dd500e6, 0x34723e0d110be55f, + 0x6206912ea1b8ce29, 0xa60f1dab018d0d02, 0xac4fc81d3e104783, 0x1bad40974a37bfaf, + 0x00000000000000df, +]; +const SUB_0_1: [u64; 9] = [ + 0x84ba52cac3ee6ee6, 0x626a174e00b9a901, 0x8f205d5df4724e69, 0x21ddab32187b139b, + 0x3291a67dd13a3c70, 0xd3bfb22886e350d6, 0xe664303e3eeb99f4, 0x0987b33f9572c856, + 0x0000000000000125, +]; +const MUL_0_1: [u64; 9] = [ + 0xc9fe37ec76abed5e, 0xf28c37faf99f76f9, 0xb7e359a692e7404c, 0xf085ef6f4e604369, + 0xca06c54beaf122b2, 0xad197cc87f2b61ce, 0x71d11f19a0c6737d, 0xbcbe539da7e29cce, + 0x000000000000004d, +]; +const ADD_0_2: [u64; 9] = [ + 0x36788ccb044c845f, 0x0af47c342af050ef, 0xba6f2938265040fb, 0xad4732dfb4624885, + 0x071ace75867e7dfe, 0xb4968912617ad6b9, 0xf9ec82d2b2d3ae79, 0xe096a01c07e67abb, + 0x00000000000001c2, +]; +const SUB_0_2: [u64; 9] = [ + 0x834d6555c735bee7, 0x86dc88a090a875a9, 0xe4203ac8dbf70e54, 0xa908b65f7524b074, + 0x8d7d6936ec748c9a, 0xc53846c126f5871f, 0x98c77588ca2832fe, 0x449e53bad7c40d4a, + 0x0000000000000041, +]; +const MUL_0_2: [u64; 9] = [ + 0xbc7c5b09c402fe98, 0x0796c9bff0a67848, 0x77f150e2bb527e58, 0x1b79203e4772b620, + 0xd0d31d5da56c8a22, 0x65d64f9ca9a18a00, 0xc65f22bc9cb803dd, 0x0c4039d6fc290863, + 0x000000000000004c, +]; +const ADD_1_2: [u64; 9] = [ + 0xb1be3a00405e1579, 0xa88a64e62a36a7ed, 0x2b4ecbda31ddf291, 0x8b6987ad9be734ea, + 0xd48927f7b544418e, 0xe0d6d6e9da9785e2, 0x1388529473e81484, 0xd70eecdc7273b265, + 0x000000000000009d, +]; +const SUB_1_2: [u64; 9] = [ + 0xba02c9a9947fb40a, 0x60283b0b198b1456, 0xd4cbdeb3de8e65bb, 0xd8b192b11bd93344, + 0x5aebc2b91b3a5024, 0xf1789498a0123649, 0xb263454a8b3c9909, 0x3b16a07b425144f3, + 0x000000000000011c, +]; +const MUL_1_2: [u64; 9] = [ + 0xecea78b68d157f8e, 0x750560668d085538, 0x8f38d46ed239d602, 0xddf7f7827c4b2d33, + 0x86254741e445d3f8, 0x08586a6147980a4a, 0x6fd47182d1ab49d9, 0xca6e20f84c4d9b99, + 0x000000000000016d, +]; + +const NEG_0: [u64; 9] = [ + 0x5e8cbe0e2b774266, 0x72cd474e2bcfe462, 0x30844f4875e5fe28, 0xa65e92e42a6c19ee, + 0xb5b3e429c6867aad, 0xc31898163bc7d113, 0xb6a603d241820f43, 0xed658614902abbfc, + 0x00000000000000fd, +]; +const INV_0: [u64; 9] = [ + 0x93a286c6686a12b3, 0x3a981f3823b3cfeb, 0xb22f2f86b1262a22, 0xbc6587cf6e99f392, + 0xe1baa4dc8fa450b6, 0xdf84af5eae83aa5a, 0x0dffa9471e228b8f, 0x8d73ccc631cd5211, + 0x0000000000000059, +]; +const NEG_1: [u64; 9] = [ + 0x27d759ba5e2d4d43, 0x998194e3a2ed45b5, 0x3fd8ab5d734ea6c1, 0x76b5b69283b7971e, + 0xe8458aa797c0b723, 0x96d84a3ec2ab21e9, 0x9d0a3410806da938, 0xf6ed3954259d8453, + 0x0000000000000022, +]; +const INV_1: [u64; 9] = [ + 0x6bfe7b8ce691f33a, 0xcffacd5693bc23e3, 0xfd019050df92d40f, 0x8a8c89ab282b53f4, + 0xf174e0c3c827b21b, 0xa12189d8ca396fcf, 0x3bc42d335b0dc0a6, 0x19dd4775e6c38723, + 0x00000000000001bd, +]; +const NEG_2: [u64; 9] = [ + 0xe1da2363f2ad014d, 0xf9a9cfeebc785a0b, 0x14a48a1151dd0c7c, 0x4f6749439f90ca63, + 0x43314d60b2fb0748, 0x8850ded762bd5833, 0x4f6d795b0baa4242, 0x3203d9cf67eec947, + 0x000000000000013f, +]; +const INV_2: [u64; 9] = [ + 0x1d19864c85afbe2e, 0x69dec1d4f00a2a49, 0xae9d8cd7f679b1e0, 0x0ed53d62cfc273be, + 0xdefd40a242d40d95, 0xdd2cb3a608535d12, 0xeec21bcf59830179, 0x51806ff643fa3421, + 0x00000000000000e9, +]; + +fn fe(limbs: [u64; 9]) -> P521ScalarField { + P521ScalarField::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + let pairs: [(usize, usize, [u64; 9], [u64; 9], [u64; 9]); 3] = [ + (0, 1, ADD_0_1, SUB_0_1, MUL_0_1), + (0, 2, ADD_0_2, SUB_0_2, MUL_0_2), + (1, 2, ADD_1_2, SUB_1_2, MUL_1_2), + ]; + for (i, j, a, s, m) in pairs { + assert_eq!(vals[i].add(&vals[j]), fe(a), "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), fe(a), "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), fe(s), "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), fe(m), "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), fe(m), "mul is commutative ({j},{i})"); + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2]; + let negs = [NEG_0, NEG_1, NEG_2]; + let invs = [INV_0, INV_1, INV_2]; + for i in 0..3 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + for &v in &vals { + assert_eq!(v.add(&P521ScalarField::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&P521ScalarField::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), P521ScalarField::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), P521ScalarField::ZERO, "x + (-x) == 0"); + assert_eq!(v.mul(&v.invert()), P521ScalarField::ONE, "x * x^-1 == 1"); + } + assert_eq!(P521ScalarField::ZERO.invert(), P521ScalarField::ZERO, "0^-1 == 0 by convention"); +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..9 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + assert_eq!(fe(N_LIMBS), P521ScalarField::ZERO); +} + +/// Regression test for a real bug: an earlier `from_limbs` reduced with a single conditional +/// subtraction, valid only for input `< 2n`. A raw `[u64; 9]` can represent values up to `2^576 - +/// 1`, some 55 bits more than `n`, so that assumption was false for genuinely wide input -- caught +/// not by this file's own (then top-limb-masked) property test, but by real ECDSA verification +/// math in the wycheproof P-521 suite. These two values -- computed independently in Python, not +/// from this crate -- are far outside the old single-subtraction's valid range. +#[test] +fn from_limbs_reduces_input_far_beyond_2n() { + let all_ones = [u64::MAX; 9]; + let expected_all_ones: [u64; 9] = [ + 0xfb7fffffffffffff, 0x28a2482470b763cd, 0x17e2251b23bb31dc, 0xca4019ff5b847b2d, + 0x02d73cbc3e206834, 0, 0, 0, 0, + ]; + assert_eq!(fe(all_ones), fe(expected_all_ones)); + + // 7*n + 12345, deliberately several multiples of n above the single-subtraction's range. + let seven_n_plus: [u64; 9] = [ + 0x200e01d5f88aec78, 0xa1f8840bc345f5c7, 0x7e9408fec14388b1, 0x3aadb49a3a4d1cf0, + 0xffffffffffffffd8, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, 0x0dff, + ]; + assert_eq!(fe(seven_n_plus), fe([12345, 0, 0, 0, 0, 0, 0, 0, 0])); +} + +#[test] +fn secret_scalar_reduces_and_round_trips() { + let secret = P521Scalar::from_limbs(VALS_0); + assert_eq!(P521ScalarField::from_secret(&secret), fe(VALS_0)); +} + +#[test] +fn secret_scalar_reduces_out_of_range_input() { + let secret = P521Scalar::from_limbs(N_LIMBS); + assert_eq!(P521ScalarField::from_secret(&secret), P521ScalarField::ZERO); +} + +#[test] +fn secret_scalar_be_bytes_round_trip() { + for limbs in [VALS_0, VALS_1, VALS_2] { + let secret = P521Scalar::from_limbs(limbs); + let round_tripped = P521Scalar::from_be_bytes(&secret.to_be_bytes()); + assert_eq!(round_tripped, secret); + } +} + +#[test] +fn public_scalar_round_trips_and_reduces() { + for limbs in [VALS_0, VALS_1, VALS_2] { + assert_eq!(P521PublicScalar::from_limbs(limbs).to_limbs(), limbs); + } + assert_eq!(P521PublicScalar::from_limbs(N_LIMBS).to_limbs(), [0; 9]); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the P-256 field test's property check. Every +/// limb, including the top one, is left as a full random `u64`: `from_limbs` must correctly reduce +/// any of the `2^576` values a raw `[u64; 9]` can represent, not just those `< 2n` -- see +/// `from_limbs_reduces_input_far_beyond_2n`'s docs for the bug an earlier version had, which this +/// deliberately-unmasked generation is what would have caught. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 9] { + let mut limbs = [0u64; 9]; + for l in limbs.iter_mut() { + *l = self.next_u64(); + } + limbs + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0xC2B2AE3D27D4EB4F); + // 1000, not 5000 -- see p521_field_tests.rs's identical property test for why. + for _ in 0..1000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != P521ScalarField::ZERO { + assert_eq!(a.mul(&a.invert()), P521ScalarField::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE0000000010); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 9], + { + let mut l = [0u64; 9]; + l[0] = 1; + l + }, + [u64::MAX; 9], + { + let mut l = [u64::MAX; 9]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `zeroize` is the signing path's way of not leaving `d`, `k` or `k^-1` legible on the stack +/// (see the method's own docs for why this type is not wrapped in `Secret` instead). Whether the +/// volatile write survives optimization cannot be observed from Rust; that the value is actually +/// cleared can be, and is what would break if the method were ever reduced to a no-op. +#[test] +fn zeroize_clears_the_value() { + let mut a = fe([1, 2, 3, 4, 5, 6, 7, 8, 9]); + assert_ne!(a, P521ScalarField::ZERO, "precondition: the value starts non-zero"); + a.zeroize(); + assert_eq!(a, P521ScalarField::ZERO, "zeroize must leave the value at zero"); + + // and it is idempotent, so a caller scrubbing twice on overlapping paths is harmless + a.zeroize(); + assert_eq!(a, P521ScalarField::ZERO); +} + +/// `is_zero` on the scalar field, pinned for both truth values and across every limb position +/// (see the field test of the same name for why); it is the check behind `negate`'s `0 -> 0` +/// special case. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(P521ScalarField::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::p521_scalar::N_LIMBS).is_zero().to_bool(), "n reduces to 0"); + assert!(!P521ScalarField::ONE.is_zero().to_bool()); + for limb_idx in 0..9 { + let mut limbs = [0u64; 9]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `n - 1`, the largest canonical scalar, is the worst case for REDC's final conditional +/// subtraction and for `add`'s carry correction, and was absent from every known-answer set +/// above (whose values are pseudorandom, so never near `n`). Expected values follow from +/// `(n-1)^2 == 1` and `(n-1) + (n-1) == n - 2` in the field, computed from `n` in Python. +#[test] +fn known_answer_at_n_minus_1() { + let n_minus_1_limbs: [u64; 9] = [ + 0xbb6fb71e91386408, 0x3bb5c9b8899c47ae, 0x7fcc0148f709a5d0, 0x51868783bf2f966b, + 0xfffffffffffffffa, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ]; + let n_minus_1 = fe(n_minus_1_limbs); + let n_minus_2 = fe([ + 0xbb6fb71e91386407, 0x3bb5c9b8899c47ae, 0x7fcc0148f709a5d0, 0x51868783bf2f966b, + 0xfffffffffffffffa, 0xffffffffffffffff, 0xffffffffffffffff, 0xffffffffffffffff, + 0x00000000000001ff, + ]); + + assert_eq!(n_minus_1.to_limbs(), n_minus_1_limbs, "Montgomery round trip of n-1"); + assert_eq!(P521PublicScalar::from_limbs(n_minus_1_limbs).to_limbs(), n_minus_1_limbs); + assert_eq!(n_minus_1.add(&n_minus_1), n_minus_2, "(n-1) + (n-1) == n-2"); + assert_eq!(n_minus_1.add(&P521ScalarField::ONE), P521ScalarField::ZERO, "(n-1) + 1 == 0"); + assert_eq!(P521ScalarField::ZERO.sub(&P521ScalarField::ONE), n_minus_1, "0 - 1 == n-1"); + assert_eq!(n_minus_1.mul(&n_minus_1), P521ScalarField::ONE, "(n-1)^2 == 1"); + assert_eq!(n_minus_1.square(), P521ScalarField::ONE, "(n-1)^2 == 1, via square"); + assert_eq!(n_minus_1.negate(), P521ScalarField::ONE, "-(n-1) == 1"); + assert_eq!(n_minus_1.invert(), n_minus_1, "(n-1)^-1 == n-1"); + assert_eq!(P521ScalarField::ONE.invert(), P521ScalarField::ONE, "1^-1 == 1"); +} + +/// The variable-time inverse on the public scalar type must agree with the constant-time +/// [`P521ScalarField::invert`] everywhere: on the known-answer values (whose inverses are pinned above +/// from Python), on `0` (both return `0` by convention), `1`, `n - 1`, and on pseudorandom +/// values -- including ones with long runs of zero bits, which are where the binary algorithm +/// takes its longest halving chains. +#[test] +fn public_scalar_invert_vartime_agrees_with_constant_time_invert() { + let n_minus_1 = { + let mut l = bouncycastle_ec::p521_scalar::N_LIMBS; + l[0] -= 1; + l + }; + let mut one = [0u64; 9]; + one[0] = 1; + // Values whose low limb is exactly 1 with higher limbs set: odd, so the binary algorithm must + // not halve them, yet indistinguishable from `1` by their low limb alone. A halving test that + // looked at more than the lowest bit (or at the wrong limb) breaks the `u == x1 * a` invariant + // on exactly these and nowhere the pseudorandom inputs below would reach. + let mut low_limb_one_a = [0u64; 9]; + low_limb_one_a[0] = 1; + low_limb_one_a[1] = 1; + let mut low_limb_one_b = [0u64; 9]; + low_limb_one_b[0] = 1; + low_limb_one_b[8] = 1; + let mut low_limb_one_c = [0u64; 9]; + low_limb_one_c[0] = 1; + low_limb_one_c[1] = u64::MAX; + let mut fixed: Vec<[u64; 9]> = + vec![[0u64; 9], one, n_minus_1, low_limb_one_a, low_limb_one_b, low_limb_one_c]; + fixed.extend([VALS_0, VALS_1, VALS_2]); + for limbs in fixed { + assert_eq!( + bouncycastle_ec::p521_scalar::P521PublicScalar::from_limbs(limbs).invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } + assert_eq!( + bouncycastle_ec::p521_scalar::P521PublicScalar::from_limbs([0u64; 9]).invert_vartime(), + P521ScalarField::ZERO, + "0^-1 == 0 by convention, matching invert" + ); + + let mut rng = Xorshift64(0x1AC0_B5EC_0000_0009); + for i in 0..500 { + let mut limbs = rng.next_limbs(); + if i % 4 == 0 { + // Sparse values: clear the low half of each limb so `u`/`v` shed many bits per step. + for limb in limbs.iter_mut() { + *limb &= 0xffff_ffff_0000_0000; + } + } + assert_eq!( + bouncycastle_ec::p521_scalar::P521PublicScalar::from_limbs(limbs).invert_vartime(), + fe(limbs).invert(), + "limbs = {limbs:x?}" + ); + } +} diff --git a/crypto/ec/tests/p521_sec1_tests.rs b/crypto/ec/tests/p521_sec1_tests.rs new file mode 100644 index 000000000..13ace4b67 --- /dev/null +++ b/crypto/ec/tests/p521_sec1_tests.rs @@ -0,0 +1,143 @@ +//! Known-answer tests for SEC 1 §2.3.3/§2.3.4 point encoding and decoding, P-521. +//! +//! `G` and `3G`'s coordinates are the same values verified independently in +//! `p521_point_tests.rs`/`p521_comb_tests.rs`; the encodings below were derived from those +//! coordinates independently in Python via plain big-endian byte packing. `G` has even `y` (tag +//! `0x02`) and `3G` has odd `y` (tag `0x03`), so between them both compressed tags are exercised. + +use bouncycastle_ec::p521::P521FieldElement; +use bouncycastle_ec::p521_sec1::{decode, decode_point, encode_compressed, encode_uncompressed}; + +const G_X: [u64; 9] = [ + 0xf97e7e31c2e5bd66, 0x3348b3c1856a429b, 0xfe1dc127a2ffa8de, 0xa14b5e77efe75928, + 0xf828af606b4d3dba, 0x9c648139053fb521, 0x9e3ecb662395b442, 0x858e06b70404e9cd, + 0x00000000000000c6, +]; +const G_Y: [u64; 9] = [ + 0x88be94769fd16650, 0x353c7086a272c240, 0xc550b9013fad0761, 0x97ee72995ef42640, + 0x17afbd17273e662c, 0x98f54449579b4468, 0x5c8a5fb42c7d1bd9, 0x39296a789a3bc004, + 0x0000000000000118, +]; +const THREE_G_X: [u64; 9] = [ + 0xa5919d2ede37ad7d, 0xaeb490862c32ea05, 0x1da6bd16b59fe21b, 0xad3f164a3a483205, + 0xe5ad7a112d7a8dd1, 0xb52a6e5b123d9ab9, 0xd91d6a64b5959479, 0x3d352443de29195d, + 0x00000000000001a7, +]; +const THREE_G_Y: [u64; 9] = [ + 0x5f588ca1ee86c0e5, 0xf105c9bc93a59042, 0x2d5aced1dec3c70c, 0x2e2dd4cf8dc575b0, + 0xd2f8ab1fa355ceec, 0xf1557fa82a9d0317, 0x979f86c6cab814f2, 0x9b03b97dfa62ddd9, + 0x000000000000013e, +]; + +const UNCOMPRESSED_G: [u8; 133] = [ + 0x04, 0x00, 0xc6, 0x85, 0x8e, 0x06, 0xb7, 0x04, 0x04, 0xe9, 0xcd, 0x9e, 0x3e, 0xcb, 0x66, 0x23, + 0x95, 0xb4, 0x42, 0x9c, 0x64, 0x81, 0x39, 0x05, 0x3f, 0xb5, 0x21, 0xf8, 0x28, 0xaf, 0x60, 0x6b, + 0x4d, 0x3d, 0xba, 0xa1, 0x4b, 0x5e, 0x77, 0xef, 0xe7, 0x59, 0x28, 0xfe, 0x1d, 0xc1, 0x27, 0xa2, + 0xff, 0xa8, 0xde, 0x33, 0x48, 0xb3, 0xc1, 0x85, 0x6a, 0x42, 0x9b, 0xf9, 0x7e, 0x7e, 0x31, 0xc2, + 0xe5, 0xbd, 0x66, 0x01, 0x18, 0x39, 0x29, 0x6a, 0x78, 0x9a, 0x3b, 0xc0, 0x04, 0x5c, 0x8a, 0x5f, + 0xb4, 0x2c, 0x7d, 0x1b, 0xd9, 0x98, 0xf5, 0x44, 0x49, 0x57, 0x9b, 0x44, 0x68, 0x17, 0xaf, 0xbd, + 0x17, 0x27, 0x3e, 0x66, 0x2c, 0x97, 0xee, 0x72, 0x99, 0x5e, 0xf4, 0x26, 0x40, 0xc5, 0x50, 0xb9, + 0x01, 0x3f, 0xad, 0x07, 0x61, 0x35, 0x3c, 0x70, 0x86, 0xa2, 0x72, 0xc2, 0x40, 0x88, 0xbe, 0x94, + 0x76, 0x9f, 0xd1, 0x66, 0x50, +]; + +const COMPRESSED_G: [u8; 67] = [ + 0x02, 0x00, 0xc6, 0x85, 0x8e, 0x06, 0xb7, 0x04, 0x04, 0xe9, 0xcd, 0x9e, 0x3e, 0xcb, 0x66, 0x23, + 0x95, 0xb4, 0x42, 0x9c, 0x64, 0x81, 0x39, 0x05, 0x3f, 0xb5, 0x21, 0xf8, 0x28, 0xaf, 0x60, 0x6b, + 0x4d, 0x3d, 0xba, 0xa1, 0x4b, 0x5e, 0x77, 0xef, 0xe7, 0x59, 0x28, 0xfe, 0x1d, 0xc1, 0x27, 0xa2, + 0xff, 0xa8, 0xde, 0x33, 0x48, 0xb3, 0xc1, 0x85, 0x6a, 0x42, 0x9b, 0xf9, 0x7e, 0x7e, 0x31, 0xc2, + 0xe5, 0xbd, 0x66, +]; + +const COMPRESSED_3G: [u8; 67] = [ + 0x03, 0x01, 0xa7, 0x3d, 0x35, 0x24, 0x43, 0xde, 0x29, 0x19, 0x5d, 0xd9, 0x1d, 0x6a, 0x64, 0xb5, + 0x95, 0x94, 0x79, 0xb5, 0x2a, 0x6e, 0x5b, 0x12, 0x3d, 0x9a, 0xb9, 0xe5, 0xad, 0x7a, 0x11, 0x2d, + 0x7a, 0x8d, 0xd1, 0xad, 0x3f, 0x16, 0x4a, 0x3a, 0x48, 0x32, 0x05, 0x1d, 0xa6, 0xbd, 0x16, 0xb5, + 0x9f, 0xe2, 0x1b, 0xae, 0xb4, 0x90, 0x86, 0x2c, 0x32, 0xea, 0x05, 0xa5, 0x91, 0x9d, 0x2e, 0xde, + 0x37, 0xad, 0x7d, +]; + +fn fe(limbs: [u64; 9]) -> P521FieldElement { + P521FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_encode_uncompressed() { + assert_eq!(encode_uncompressed(&fe(G_X), &fe(G_Y)), UNCOMPRESSED_G); +} + +#[test] +fn known_answer_encode_compressed() { + assert_eq!(encode_compressed(&fe(G_X), &fe(G_Y)), COMPRESSED_G, "G has even y -> tag 0x02"); + assert_eq!( + encode_compressed(&fe(THREE_G_X), &fe(THREE_G_Y)), + COMPRESSED_3G, + "3G has odd y -> tag 0x03" + ); +} + +#[test] +fn known_answer_decode_uncompressed() { + let (x, y) = decode(&UNCOMPRESSED_G).expect("valid uncompressed point"); + assert_eq!(x, fe(G_X)); + assert_eq!(y, fe(G_Y)); +} + +#[test] +fn known_answer_decode_compressed() { + let (x, y) = decode(&COMPRESSED_G).expect("valid compressed point, tag 0x02"); + assert_eq!(x, fe(G_X)); + assert_eq!(y, fe(G_Y)); + + let (x3, y3) = decode(&COMPRESSED_3G).expect("valid compressed point, tag 0x03"); + assert_eq!(x3, fe(THREE_G_X)); + assert_eq!(y3, fe(THREE_G_Y)); +} + +#[test] +fn round_trip_uncompressed_and_compressed() { + for (x, y) in [(G_X, G_Y), (THREE_G_X, THREE_G_Y)] { + let uncompressed = encode_uncompressed(&fe(x), &fe(y)); + assert_eq!(decode(&uncompressed), Some((fe(x), fe(y)))); + + let compressed = encode_compressed(&fe(x), &fe(y)); + assert_eq!(decode(&compressed), Some((fe(x), fe(y)))); + + assert!(decode_point(&uncompressed).unwrap().to_affine().is_some()); + } +} + +#[test] +fn decode_rejects_malformed_input() { + assert_eq!(decode(&[]), None); + assert_eq!(decode(&[0x04; 132]), None); + assert_eq!(decode(&[0x04; 134]), None); + assert_eq!(decode(&[0x02; 32]), None); + + let mut bad_tag_133 = UNCOMPRESSED_G; + bad_tag_133[0] = 0x02; + assert_eq!(decode(&bad_tag_133), None); + + let mut bad_tag_67 = COMPRESSED_G; + bad_tag_67[0] = 0x04; + assert_eq!(decode(&bad_tag_67), None); + + assert_eq!(decode(&[0x00]), None); + + let mut x_too_big = COMPRESSED_G; + x_too_big[1..67].fill(0xff); + assert_eq!(decode(&x_too_big), None); + + let mut no_root = COMPRESSED_G; + no_root[1..67].fill(0); + no_root[66] = 3; // x = 3, confirmed (in the Python derivation) to have no square root + assert_eq!(decode(&no_root), None); + + let mut not_on_curve = UNCOMPRESSED_G; + not_on_curve[132] ^= 0x01; + assert_eq!(decode(¬_on_curve), None); + + let mut y_too_big = UNCOMPRESSED_G; + y_too_big[67..133].fill(0xff); + assert_eq!(decode(&y_too_big), None); +} diff --git a/crypto/ec/tests/p521_wnaf_tests.rs b/crypto/ec/tests/p521_wnaf_tests.rs new file mode 100644 index 000000000..9ee3166da --- /dev/null +++ b/crypto/ec/tests/p521_wnaf_tests.rs @@ -0,0 +1,187 @@ +//! Known-answer tests for [`shamir_multiply`] (`[u]G + [v]Q`), P-521. + +use bouncycastle_ec::p521::P521FieldElement; +use bouncycastle_ec::p521_comb::comb_multiply_base_point; +use bouncycastle_ec::p521_point::P521JacobianPoint; +use bouncycastle_ec::p521_scalar::{P521PublicScalar, P521Scalar}; +use bouncycastle_ec::p521_wnaf::shamir_multiply; + +const Q_X: [u64; 9] = [ + 0xa64dc52cc15bfb04, 0xaca0b282aaa81dc2, 0xeef36ecaa8b95fe2, 0x687b823dcebc81f1, + 0xa4f6dffd0e8257f2, 0xc757f9716fb22e1e, 0x48376daf81b48bd7, 0xeac46a481ec62225, + 0x000000000000014c, +]; +const Q_Y: [u64; 9] = [ + 0xca6b644c91981f41, 0x815110666aafb2a8, 0xdb15842e498fede2, 0x6f032bd2342ac842, + 0xa0790c480d52a23d, 0x13753ef7ed3683e7, 0x9b60a50747dfe744, 0xb12cb10e2c79060b, + 0x0000000000000160, +]; + +fn q() -> P521JacobianPoint { + P521JacobianPoint::from_affine( + P521FieldElement::from_limbs(Q_X), + P521FieldElement::from_limbs(Q_Y), + ) +} + +fn scalar(limbs: [u64; 9]) -> P521PublicScalar { + P521PublicScalar::from_limbs(limbs) +} + +fn assert_result_is(u: [u64; 9], v: [u64; 9], expected_x: [u64; 9], expected_y: [u64; 9]) { + let result = shamir_multiply(&scalar(u), &scalar(v), &q()); + let (rx, ry) = result.to_affine().expect("expected a non-infinity result"); + assert_eq!(rx, P521FieldElement::from_limbs(expected_x), "x mismatch for u={u:x?} v={v:x?}"); + assert_eq!(ry, P521FieldElement::from_limbs(expected_y), "y mismatch for u={u:x?} v={v:x?}"); +} + +#[test] +fn known_answer_large_u_v() { + let u: [u64; 9] = [ + 0x76502dbbd55de013, 0x9eecd37a3b49e19e, 0x33c315cc5803ef43, 0x062c9409f666b7bb, + 0xdaaee220685cc19c, 0x5a8b7626862337f8, 0x707a0f19c1784037, 0xa6109ce0ee8496ac, + 0x0000000000000088, + ]; + let v: [u64; 9] = [ + 0xdec5a82f3a0a30f2, 0xab186cbe22cad4c8, 0x6935acae3101699c, 0x745bcf1d6dd90512, + 0xf7548fb55be98622, 0xb47f1faf203dc251, 0x983d20175f54f0d4, 0x86b5248cbebec284, + 0x000000000000001c, + ]; + assert_result_is( + u, + v, + [ + 0xe5d48e7b0f6594fa, 0x0d524f16894beedd, 0x133673b941b3de0a, 0x8d9d402465e261ff, + 0xbb5175b48d0efce6, 0xe8b9ff83af1eb56c, 0x82cb7654a02c7f2b, 0xd962dad7ed0d86ad, + 0x00000000000001b2, + ], + [ + 0x0911be7f166dfedb, 0x078a2b1a04c37f02, 0xa18460fbe772b99f, 0x4bc7e5bbd5a938f0, + 0xb45f5980510db99d, 0x32fce6270286d0a6, 0x9b9f32c1e418a606, 0xa6197f50f7c2b1ea, + 0x0000000000000148, + ], + ); +} + +#[test] +fn edge_case_v_is_zero_reduces_to_u_times_g() { + assert_result_is( + [5, 0, 0, 0, 0, 0, 0, 0, 0], + [0; 9], + [ + 0xd5ab5096ec8f3078, 0x29d7e1e6d8931738, 0x7112feaf137e79a3, 0x383c0c6d5e301423, + 0xcf03dab8f177ace4, 0x7a596efdb53f0d24, 0x3dbc3391c04eb0bf, 0x2bf3c52927a432c7, + 0x0000000000000065, + ], + [ + 0x173cc3e8deb090cb, 0xd1f007257354f7f8, 0x311540211cf5ff79, 0xbb6897c9072cf374, + 0xedd817c9a0347087, 0x1cd8fe8e872e0051, 0x8a2b73114a811291, 0xe6ef1bdd6601d6ec, + 0x000000000000015b, + ], + ); +} + +#[test] +fn edge_case_u_is_zero_reduces_to_v_times_q() { + assert_result_is( + [0; 9], + [5, 0, 0, 0, 0, 0, 0, 0, 0], + [ + 0x75ca491b7f07e70d, 0xe6dc7f1588257d2f, 0x86abc77f0ad29024, 0x6070155d58b40f74, + 0xd607266aacea7f73, 0x06dfbb1d0dc9db85, 0x798817d84830c87a, 0xc4c41c58ccb8e7c6, + 0x0000000000000017, + ], + [ + 0xd8d09be4d547efa5, 0x47d22081c3c28c9f, 0xe54fe9c561252dcc, 0x80ca5e370551d128, + 0x8b12f279ffe7bd20, 0x71e701c2ed4aa80c, 0x05c3c429fd341f48, 0x3bbea45f73d14f84, + 0x00000000000000ab, + ], + ); +} + +#[test] +fn edge_case_both_zero_is_infinity() { + let result = shamir_multiply(&scalar([0; 9]), &scalar([0; 9]), &q()); + assert!(result.to_affine().is_none()); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 9] { + let mut limbs = [0u64; 9]; + for l in limbs.iter_mut() { + *l = self.next_u64(); + } + limbs[8] &= 0x1ff; + limbs + } +} + +#[test] +fn cross_checked_against_the_comb_multiplier_over_many_pseudorandom_scalars() { + // shamir_multiply(k, 0, anything) must equal comb_multiply_base_point(k): two independently + // implemented multipliers (fixed-base comb vs. interleaved wNAF) computing the same [k]G. + // 100, not the 500 used for P-256/P-384: P-521's per-multiply cost is higher (9-limb field + // ops, 522-round comb/wNAF loops), and this test already shares its coverage goal with the + // KATs and edge cases above. + let mut rng = Xorshift64(0xA5A5A5A5A5A5A5A5); + for _ in 0..100 { + let k_limbs = rng.next_limbs(); + let via_comb = comb_multiply_base_point(&P521Scalar::from_limbs(k_limbs)); + let via_wnaf = + shamir_multiply(&P521PublicScalar::from_limbs(k_limbs), &scalar([0; 9]), &q()); + assert_eq!(via_comb.to_affine(), via_wnaf.to_affine(), "k = {k_limbs:x?}"); + } +} + +/// Every other test in this file uses one fixed `Q = dG`. The three `Q` values here each take a +/// path that one cannot: `Q = infinity` makes every `[v]Q` digit an addition of the identity, +/// `Q = G` makes the two precomputed odd-multiple tables identical (so the interleaved +/// additions hit the same-point case of `add_vartime`), and `Q = -G` makes them negatives of +/// each other (the opposite-point case, and with `u == v` a final result of infinity). Expected +/// values come from the fixed-base comb multiplier on the combined scalar, which is pinned by +/// its own known-answer tests. +#[test] +fn edge_case_q_is_infinity_g_or_minus_g() { + let g = P521JacobianPoint::from_affine( + P521FieldElement::from_limbs(bouncycastle_ec::p521_domain::G_X_LIMBS), + P521FieldElement::from_limbs(bouncycastle_ec::p521_domain::G_Y_LIMBS), + ); + let mut u = [0u64; 9]; + u[0] = 12345; + let mut v = [0u64; 9]; + v[0] = 6789; + let mut u_plus_v = [0u64; 9]; + u_plus_v[0] = 19134; + let mut u_minus_v = [0u64; 9]; + u_minus_v[0] = 5556; + let via_comb = |k: [u64; 9]| comb_multiply_base_point(&P521Scalar::from_limbs(k)).to_affine(); + + // [u]G + [v]infinity == [u]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &P521JacobianPoint::INFINITY).to_affine(), + via_comb(u) + ); + // [u]G + [v]G == [u+v]G + assert_eq!(shamir_multiply(&scalar(u), &scalar(v), &g).to_affine(), via_comb(u_plus_v)); + // [u]G + [v](-G) == [u-v]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &g.negate()).to_affine(), + via_comb(u_minus_v) + ); + // [u]G + [u](-G) == infinity + assert!(shamir_multiply(&scalar(u), &scalar(u), &g.negate()).is_infinity().to_bool()); + // [0]G + [v]G == [v]G: the G table is never consulted, only Q's + assert_eq!(shamir_multiply(&scalar([0u64; 9]), &scalar(v), &g).to_affine(), via_comb(v)); +} diff --git a/crypto/ec/tests/sm2_comb_tests.rs b/crypto/ec/tests/sm2_comb_tests.rs new file mode 100644 index 000000000..3f642ead1 --- /dev/null +++ b/crypto/ec/tests/sm2_comb_tests.rs @@ -0,0 +1,70 @@ +//! Known-answer tests for [`comb_multiply_base_point`] (`[k]G`), SM2. +//! +//! `k = 2, 3` reuse the values verified independently in `sm2_point_tests.rs`; `k` for +//! `known_answer_large_k` and `n-1` (where `[k]G = -G`) were computed the same way (Python affine +//! group law, `a = -3`), `random.seed(628917)` for the former. + +use bouncycastle_ec::sm2::Sm2FieldElement; +use bouncycastle_ec::sm2_comb::comb_multiply_base_point; +use bouncycastle_ec::sm2_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::sm2_scalar::Sm2Scalar; + +fn scalar(limbs: [u64; 4]) -> Sm2Scalar { + Sm2Scalar::from_limbs(limbs) +} + +fn assert_result_is(k_limbs: [u64; 4], x: [u64; 4], y: [u64; 4]) { + let result = comb_multiply_base_point(&scalar(k_limbs)); + let (rx, ry) = result.to_affine().expect("[k]G must not be infinity for k in (0, n)"); + assert_eq!(rx, Sm2FieldElement::from_limbs(x), "x mismatch for k = {k_limbs:x?}"); + assert_eq!(ry, Sm2FieldElement::from_limbs(y), "y mismatch for k = {k_limbs:x?}"); +} + +#[test] +fn known_answer_small_k() { + assert_result_is([1, 0, 0, 0], G_X_LIMBS, G_Y_LIMBS); + assert_result_is( + [2, 0, 0, 0], + [0x495c2e1da3f2bd52, 0x9c0dfa08c08a7331, 0x0d58ef57fa73ba4d, 0x56cefd60d7c87c00], + [0x6f780d3a970a23c3, 0x6de84c182f6c8e71, 0x68535ce0f8eaf1bd, 0x31b7e7e6cc8189f6], + ); + assert_result_is( + [3, 0, 0, 0], + [0xe26918f1d0509ebf, 0xa13f6bd945302244, 0xbe2daa8cdb41e24c, 0xa97f7cd4b3c993b4], + [0xaaacdd037458f6e6, 0x7c400ee5cd045292, 0xccc5cec08a72150f, 0x530b5dd88c688ef5], + ); +} + +#[test] +fn known_answer_large_k() { + let k: [u64; 4] = + [0x390615907482bbf0, 0xee561b21ad65d466, 0x7ed50428cefb0fcc, 0xc876addf81b5d6d3]; + assert_result_is( + k, + [0x7b1a3e3203faefbd, 0x373334e5db156744, 0x1a22334ca73760b7, 0xdb74a8b512b45996], + [0x1d5911cd8455f119, 0xf8ec3aa79e7f63b5, 0xa765d19380fe040d, 0x6107f4d9950060d7], + ); +} + +#[test] +fn edge_case_k_equals_n_minus_1() { + // (n-1)*G == -G: same x as G, negated y. + let n_minus_1: [u64; 4] = + [0x53bbf40939d54122, 0x7203df6b21c6052b, 0xffffffffffffffff, 0xfffffffeffffffff]; + assert_result_is( + n_minus_1, + G_X_LIMBS, + [0xfd20cd1adec60f5f, 0x2f56788239d5b8c0, 0xa642311c9496deac, 0x43c8c95c0b098863], + ); +} + +/// `k = 0` is the one scalar for which `[k]G` is the identity: every comb digit is `0`, so every +/// round selects table entry `0` (the infinity sentinel) and the accumulator never leaves +/// `INFINITY`. Nothing in the signing path can produce it (`k` is in `[1, n-1]` by construction), +/// which is exactly why it needs pinning here rather than being left to chance. +#[test] +fn edge_case_k_equals_zero_is_infinity() { + let result = comb_multiply_base_point(&scalar([0u64; 4])); + assert!(result.is_infinity().to_bool()); + assert!(result.to_affine().is_none()); +} diff --git a/crypto/ec/tests/sm2_field_tests.rs b/crypto/ec/tests/sm2_field_tests.rs new file mode 100644 index 000000000..5b85abf58 --- /dev/null +++ b/crypto/ec/tests/sm2_field_tests.rs @@ -0,0 +1,327 @@ +//! Known-answer tests for [`Sm2FieldElement`] arithmetic. Expected values computed independently +//! in Python: +//! +//! ```text +//! p = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF +//! random.seed(2025256) +//! vals = [random.randrange(1, p) for _ in range(3)] +//! ``` + +use bouncycastle_ec::sm2::Sm2FieldElement; + +const VALS_0: [u64; 4] = + [0xa5a1f45ea143fd85, 0xb169fcb29ca305fd, 0xb041efa5f7f9075e, 0x8273a708ddcf1813]; +const VALS_1: [u64; 4] = + [0x5d4318d45d5f398b, 0x66a31f481118c121, 0x9b935fea7664e927, 0x9c69a61bd28edef4]; +const VALS_2: [u64; 4] = + [0xd72fd065c85ddd2a, 0x054e35f32200b154, 0x4bf4e4bd8a7637ed, 0x0ffca7096cc68ba9]; + +const ADD_0_1: [u64; 4] = + [0x02e50d32fea33711, 0x180d1bfbadbbc71e, 0x4bd54f906e5df086, 0x1edd4d25b05df708]; +const SUB_0_1: [u64; 4] = + [0x485edb8a43e4c3f9, 0x4ac6dd698b8a44dd, 0x14ae8fbb81941e37, 0xe60a00ec0b40391f]; +const MUL_0_1: [u64; 4] = + [0x22a1478fb52c780e, 0x784fac3d31786ba8, 0x42a03a2563709066, 0x4c3e29f24dca72aa]; +const ADD_0_2: [u64; 4] = + [0x7cd1c4c469a1daaf, 0xb6b832a5bea3b752, 0xfc36d463826f3f4b, 0x92704e124a95a3bc]; +const SUB_0_2: [u64; 4] = + [0xce7223f8d8e6205b, 0xac1bc6bf7aa254a8, 0x644d0ae86d82cf71, 0x7276ffff71088c6a]; +const MUL_0_2: [u64; 4] = + [0xd8a3fa50e531f3c2, 0xb93450874072c8b8, 0x8af50b9cd5261363, 0x17d5bc770d7fb2bf]; +const ADD_1_2: [u64; 4] = + [0x3472e93a25bd16b5, 0x6bf1553b33197276, 0xe78844a800db2114, 0xac664d253f556a9d]; +const SUB_1_2: [u64; 4] = + [0x8613486e95015c61, 0x6154e954ef180fcc, 0x4f9e7b2cebeeb13a, 0x8c6cff1265c8534b]; +const MUL_1_2: [u64; 4] = + [0x89f03ef7a609798b, 0x68d0d8512c1ba481, 0x8d119a103a42baf4, 0x7c08a7922d7bce22]; + +const NEG_0: [u64; 4] = + [0x5a5e0ba15ebc027a, 0x4e96034c635cfa03, 0x4fbe105a0806f8a1, 0x7d8c58f62230e7ec]; +const INV_0: [u64; 4] = + [0x5251f7c6fd8bcb7a, 0x2d5d57166464d1b1, 0x993bb82ff6813b65, 0xb7a104986836f5a2]; +const NEG_1: [u64; 4] = + [0xa2bce72ba2a0c674, 0x995ce0b6eee73edf, 0x646ca015899b16d8, 0x639659e32d71210b]; +const INV_1: [u64; 4] = + [0x2bab29fd023acd08, 0xca14cd4dc683f729, 0xdbaacf938a4fce8e, 0x509a135380735a7c]; +const NEG_2: [u64; 4] = + [0x28d02f9a37a222d5, 0xfab1ca0bddff4eac, 0xb40b1b427589c812, 0xf00358f593397456]; +const INV_2: [u64; 4] = + [0xe8af5ae64649385b, 0x6a4ae6428703f329, 0xa3206cd2ad67c05f, 0x4af82ae209016e95]; + +fn fe(limbs: [u64; 4]) -> Sm2FieldElement { + Sm2FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + let pairs: [(usize, usize, [u64; 4], [u64; 4], [u64; 4]); 3] = [ + (0, 1, ADD_0_1, SUB_0_1, MUL_0_1), + (0, 2, ADD_0_2, SUB_0_2, MUL_0_2), + (1, 2, ADD_1_2, SUB_1_2, MUL_1_2), + ]; + for (i, j, a, s, m) in pairs { + assert_eq!(vals[i].add(&vals[j]), fe(a), "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), fe(a), "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), fe(s), "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), fe(m), "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), fe(m), "mul is commutative ({j},{i})"); + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2]; + let negs = [NEG_0, NEG_1, NEG_2]; + let invs = [INV_0, INV_1, INV_2]; + for i in 0..3 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2].map(fe); + for &v in &vals { + assert_eq!(v.add(&Sm2FieldElement::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&Sm2FieldElement::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), Sm2FieldElement::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), Sm2FieldElement::ZERO, "x + (-x) == 0"); + assert_eq!(v.mul(&v.invert()), Sm2FieldElement::ONE, "x * x^-1 == 1"); + } +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..4 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + use bouncycastle_ec::sm2::P_LIMBS; + assert_eq!(fe(P_LIMBS), Sm2FieldElement::ZERO); +} + +/// xorshift64* PRNG, fixed seed: deterministic across runs, needs no external dependency. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0x5252525252525252); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != Sm2FieldElement::ZERO { + assert_eq!(a.mul(&a.invert()), Sm2FieldElement::ONE, "a * a^-1 == 1"); + } + } +} + +/// Products chosen to drive `reduce`'s five-limb accumulator's top limb across every value +/// it can actually take (4 through 17 here; the bound proved in `reduce`'s own doc comment is +/// 18), so the top-limb fold and the final conditional subtraction are both +/// exercised at their extremes rather than only in the middle of their range. Ordinary +/// pseudorandom operands almost never reach the ends: these were found by searching ~900,000 +/// products, including ones biased towards all-ones high words. Expected values are Python's +/// `(a * b) % p`, computed independently of this crate's arithmetic. +#[test] +fn known_answer_mul_at_the_reduction_bounds() { + // (a, b, a*b mod p), one per reachable top-limb value, ascending. + let cases: [([u64; 4], [u64; 4], [u64; 4]); 14] = [ + // top limb 4 + ( + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + ), + // top limb 5 + ( + [0xfffffffffffffffe, 0xffffffff00000000, 0xffffffffffffffff, 0xfffffffeffffffff], + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + [0xfffffffffffffffe, 0xffffffff00000000, 0xffffffffffffffff, 0xfffffffeffffffff], + ), + // top limb 6 + ( + [0x33acc967df52efc7, 0x4fc1db5f2d8f3e07, 0x3be79cc15726eb41, 0x4acdf8fb1f50e4ee], + [0xe698a0b713235314, 0x126146b6bd8dc7dc, 0x64b6da27c8fc0161, 0x40c2450df5c15810], + [0x202c17c130933e53, 0x93bb3a5c05e5ec9b, 0xf355099dc623c874, 0xbcf5484c97d86554], + ), + // top limb 7 + ( + [0x7030a50db754f89a, 0x8769ca02ea303e0d, 0x65e987940ba28ac9, 0x16cabd95e22dbc49], + [0xeaeecfb77a462a47, 0x200ada18f2088666, 0xd86c0bc0f4cee0ac, 0x58a43d2b2d2e7fc6], + [0xee7ac5c13d33448a, 0x1e5ac068ced0478d, 0x8e405143688797e9, 0x723010bdf644c241], + ), + // top limb 8 + ( + [0xc79c6ae82a4ddcd8, 0x32edb25757d1a11b, 0x51f24f89eb9d46ea, 0xd60bec7706f8e4f3], + [0x9b75cd625142260a, 0xbffedee5673265bf, 0x32737e9a11dd0076, 0x0c61cb71776a0c7d], + [0xe5fe086f5e8659dd, 0xbadd10879dc3bcad, 0xfb842ae9198d41d5, 0xeda9e6e9234a4780], + ), + // top limb 9 + ( + [0x6c15a5b7a31b9a88, 0xad60872bf7608860, 0x614e0ac60406e6c8, 0x3f35c1e61fcd76ff], + [0xfb2dc41468f25be3, 0x395bc78b8b245993, 0xff2fa10e3f16ca83, 0xfe188bb380f05577], + [0x7264eb6ad238cd0e, 0x201c0d755c2682dd, 0x482b8e0a56ae3670, 0xa782a032109e3bf9], + ), + // top limb 10 + ( + [0x3b8104488f2d8e22, 0x598dd6fa99c39428, 0xc161a8733a085adb, 0xffe7bdaff61a76c4], + [0xeb9029f855dc1b56, 0x38f25c15a4af1600, 0xa6f43ed979d8ede6, 0x2227407b18226e55], + [0x1f56e64ff2e30e9b, 0x590c81056d431a75, 0x5c73a8e0a26cbc09, 0x3438eb3dafc32768], + ), + // top limb 11 + ( + [0x42c7f4e1c0e0861c, 0xef54ec651388b2e2, 0xf4de87fa4830146c, 0x71e39cf9a0ce11e0], + [0xe949f0e05fd3df71, 0xa56faa8a75a9f029, 0xb642126f51468661, 0x82dc965565f17d15], + [0x47f79ca7ce88003b, 0xdcd3336c11ea2ca5, 0x37e16c292ff9cf37, 0xb1d7dda6fa4fda60], + ), + // top limb 12 + ( + [0xd8c527101686c8dd, 0x3969aaaea1ef7f3d, 0x12340033bd89d876, 0xbd01b5599f65d691], + [0xfad40f4becb9c562, 0xc31a25b3f569ad7e, 0xe1a9bf6a8a30bb6b, 0xbef2eb0a5a696886], + [0xdb635b24042727b1, 0x632417b5038d7677, 0xc0a0d36d47d08092, 0x553574b2d67ef0b6], + ), + // top limb 13 + ( + [0x6c77da1c15ab106b, 0x45389e945635d4a8, 0x72330b666db8f163, 0xe094e7425acc5486], + [0xf05a046171a9f871, 0xe41082c1e2b853a3, 0x68ed177d17a99c38, 0xc31c5e8e401cffae], + [0xa120e10e9e05550e, 0x85c78a523499599d, 0x53fefd01687260e6, 0x4776539506828316], + ), + // top limb 14 + ( + [0xfffffffffffffffe, 0xffffffff00000000, 0xffffffffffffffff, 0xfffffffeffffffff], + [0xfffffffffffffffe, 0xffffffff00000000, 0xffffffffffffffff, 0xfffffffeffffffff], + [0x0000000000000001, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000], + ), + // top limb 15 + ( + [0x422e012c71efc130, 0xaf9f500c9b843cff, 0x35ed557e70de52b2, 0xf8117bd39a627a5d], + [0xc5e7878d071a87d9, 0xa9661930992c7856, 0xcfe94313890735ba, 0xd7f003fcc8760cb4], + [0xda27792522c7096a, 0x8d9c12d268e4bdd5, 0x78a6f489d79a2e93, 0x39c44b39d3676947], + ), + // top limb 16 + ( + [0x19f1eef25e979f0b, 0x98c338454a8ebf94, 0xd3ade626e6492ef2, 0xf6efa94e0854aa85], + [0x39b0369ebeb3d1ae, 0x88ff2fa60a290270, 0x86b7590537824835, 0xc5a61e2646e26036], + [0x9d545cf849a44895, 0x01a8480ed66873aa, 0x93408ccf38921617, 0x5f38ac6852629955], + ), + // top limb 17 + ( + [0x82b76b3e7be5d265, 0x53b7d7a65091ebbf, 0xd43a1492f17c0abc, 0xb44383d7925bfff9], + [0x428a7dcb2f5f4779, 0x7904375252397080, 0x41a1d2610e45d683, 0xfee086171cf0cf63], + [0x888fd29356771e3d, 0x4e88d54595b59b9a, 0x6e411bba9d44960d, 0x0c4c6fc746169b40], + ), + ]; + for (a, b, expected) in cases { + assert_eq!(fe(a).mul(&fe(b)), fe(expected), "a = {a:x?}, b = {b:x?}"); + assert_eq!(fe(b).mul(&fe(a)), fe(expected), "commuted: a = {a:x?}, b = {b:x?}"); + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE0000000003); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 4], + { + let mut l = [0u64; 4]; + l[0] = 1; + l + }, + [u64::MAX; 4], + { + let mut l = [u64::MAX; 4]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `is_zero` is the mask every exceptional-case select in this crate's point arithmetic keys +/// off (infinity detection, the `H == 0` / `R == 0` same-point and opposite-point cases), and +/// otherwise exercised only through those selects. Both truth values are pinned here, with +/// the nonzero side walked across every limb position so a mask that inspected only some limbs +/// would be caught. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(Sm2FieldElement::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::sm2::P_LIMBS).is_zero().to_bool(), "p reduces to 0"); + assert!(!Sm2FieldElement::ONE.is_zero().to_bool()); + for limb_idx in 0..4 { + let mut limbs = [0u64; 4]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `add`'s carry-out correction and `sub`'s borrow correction, pinned at the operands that force +/// them: `(p-1) + (p-1)` is the largest sum two canonical elements can form, and `0 - 1` the +/// smallest difference. The expected values are `p - 2` and `p - 1` themselves (computed from +/// `p` in Python), so this is a check of the wrap-around handling, not of the digits of `p`. +#[test] +fn known_answer_add_carry_and_sub_borrow_at_the_extremes() { + let p_minus_1 = + fe([0xfffffffffffffffe, 0xffffffff00000000, 0xffffffffffffffff, 0xfffffffeffffffff]); + let p_minus_2 = + fe([0xfffffffffffffffd, 0xffffffff00000000, 0xffffffffffffffff, 0xfffffffeffffffff]); + let two = fe([0x0000000000000002, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]); + + assert_eq!(p_minus_1.add(&p_minus_1), p_minus_2, "(p-1) + (p-1) == p-2"); + assert_eq!(p_minus_1.add(&Sm2FieldElement::ONE), Sm2FieldElement::ZERO, "(p-1) + 1 == 0"); + assert_eq!(Sm2FieldElement::ZERO.sub(&Sm2FieldElement::ONE), p_minus_1, "0 - 1 == p-1"); + assert_eq!(Sm2FieldElement::ONE.sub(&p_minus_1), two, "1 - (p-1) == 2"); + assert_eq!(p_minus_1.negate(), Sm2FieldElement::ONE, "-(p-1) == 1"); + assert_eq!(Sm2FieldElement::ONE.invert(), Sm2FieldElement::ONE, "1^-1 == 1"); + assert_eq!(p_minus_1.invert(), p_minus_1, "(p-1)^-1 == p-1, since (p-1)^2 == 1"); +} diff --git a/crypto/ec/tests/sm2_point_tests.rs b/crypto/ec/tests/sm2_point_tests.rs new file mode 100644 index 000000000..053b43166 --- /dev/null +++ b/crypto/ec/tests/sm2_point_tests.rs @@ -0,0 +1,107 @@ +//! Known-answer tests for [`Sm2JacobianPoint`] arithmetic. +//! +//! `G`, `2G`, `3G` were computed independently in Python via the standard affine group law +//! (`a = -3`), not from this crate, and cross-checked against `n*G == infinity`: +//! +//! ```text +//! p = 0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF +//! a = p - 3 +//! # affine add/double via the standard a = -3 formulas +//! ``` + +use bouncycastle_ec::sm2::Sm2FieldElement; +use bouncycastle_ec::sm2_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::sm2_point::Sm2JacobianPoint; + +const TWO_G_X: [u64; 4] = + [0x495c2e1da3f2bd52, 0x9c0dfa08c08a7331, 0x0d58ef57fa73ba4d, 0x56cefd60d7c87c00]; +const TWO_G_Y: [u64; 4] = + [0x6f780d3a970a23c3, 0x6de84c182f6c8e71, 0x68535ce0f8eaf1bd, 0x31b7e7e6cc8189f6]; +const THREE_G_X: [u64; 4] = + [0xe26918f1d0509ebf, 0xa13f6bd945302244, 0xbe2daa8cdb41e24c, 0xa97f7cd4b3c993b4]; +const THREE_G_Y: [u64; 4] = + [0xaaacdd037458f6e6, 0x7c400ee5cd045292, 0xccc5cec08a72150f, 0x530b5dd88c688ef5]; + +fn fe(limbs: [u64; 4]) -> Sm2FieldElement { + Sm2FieldElement::from_limbs(limbs) +} + +fn g() -> Sm2JacobianPoint { + Sm2JacobianPoint::from_affine(fe(G_X_LIMBS), fe(G_Y_LIMBS)) +} + +#[test] +fn known_answer_double() { + let doubled = g().double(); + let (x, y) = doubled.to_affine().unwrap(); + assert_eq!(x, fe(TWO_G_X)); + assert_eq!(y, fe(TWO_G_Y)); +} + +#[test] +fn known_answer_add() { + let sum = g().add(&g().double()); + let (x, y) = sum.to_affine().unwrap(); + assert_eq!(x, fe(THREE_G_X)); + assert_eq!(y, fe(THREE_G_Y)); +} + +#[test] +fn point_plus_negation_is_infinity() { + let g = g(); + assert!(g.add(&g.negate()).is_infinity().to_bool()); +} + +#[test] +fn infinity_identities() { + let g = g(); + let infinity = Sm2JacobianPoint::INFINITY; + assert!(infinity.is_infinity().to_bool()); + assert!(infinity.to_affine().is_none()); + + let (x1, y1) = g.add(&infinity).to_affine().unwrap(); + let (x2, y2) = g.to_affine().unwrap(); + assert_eq!((x1, y1), (x2, y2), "P + infinity == P"); + + let (x3, y3) = infinity.add(&g).to_affine().unwrap(); + assert_eq!((x3, y3), (x2, y2), "infinity + P == P"); + + assert!(infinity.add(&infinity).is_infinity().to_bool()); +} + +#[test] +fn scaled_z_representation_still_adds_correctly() { + // (X, Y, Z) and (X*c^2, Y*c^3, Z*c) represent the same affine point for any nonzero c; the + // branch-free add/double formulas must agree regardless of which representative is used. + let c = fe([7, 0, 0, 0]); + let c2 = c.mul(&c); + let c3 = c2.mul(&c); + let g = g(); + let scaled = Sm2JacobianPoint { x: g.x.mul(&c2), y: g.y.mul(&c3), z: g.z.mul(&c) }; + + let canonical_double = g.double().to_affine().unwrap(); + let scaled_double = scaled.double().to_affine().unwrap(); + assert_eq!(canonical_double, scaled_double); + + let canonical_sum = g.add(&g.double()).to_affine().unwrap(); + let scaled_sum = scaled.add(&g.double()).to_affine().unwrap(); + assert_eq!(canonical_sum, scaled_sum); +} + +/// `is_infinity` was previously only ever asserted true. The false side is pinned on the base +/// point and on values derived from it, and doubling the identity must stay at the identity +/// (`double` is total, per its docs, and the comb multiplier's accumulator starts there). +#[test] +fn is_infinity_is_false_for_finite_points_and_infinity_doubles_to_itself() { + let g = g(); + assert!(!g.is_infinity().to_bool(), "G is not the identity"); + assert!(!g.double().is_infinity().to_bool(), "2G is not the identity"); + assert!(!g.negate().is_infinity().to_bool(), "-G is not the identity"); + assert!(!g.add(&g.double()).is_infinity().to_bool(), "3G is not the identity"); + + let inf = Sm2JacobianPoint::INFINITY; + assert!(inf.double().is_infinity().to_bool(), "2 * infinity == infinity"); + assert!(inf.double().to_affine().is_none()); + assert!(inf.negate().is_infinity().to_bool(), "-infinity == infinity"); + assert!(inf.double().add(&g).to_affine().is_some(), "2 * infinity + G == G, a finite point"); +} diff --git a/crypto/ec/tests/sm2_scalar_tests.rs b/crypto/ec/tests/sm2_scalar_tests.rs new file mode 100644 index 000000000..ebedd093a --- /dev/null +++ b/crypto/ec/tests/sm2_scalar_tests.rs @@ -0,0 +1,455 @@ +//! Known-answer tests for [`Sm2ScalarField`] arithmetic mod the SM2 curve order `n`. +//! Expected values computed independently in Python: +//! +//! ```text +//! n = 0xfffffffeffffffffffffffffffffffff7203df6b21c6052b53bbf40939d54123 +//! random.seed(830917) +//! vals = [random.randrange(1, n) for _ in range(5)] + [0] +//! # for each pair (i, j) with i < j: (vals[i]+vals[j]) % n, (vals[i]-vals[j]) % n, (vals[i]*vals[j]) % n +//! # for each i: (-vals[i]) % n, pow(vals[i], n-2, n) if vals[i] != 0 else 0 +//! ``` + +use bouncycastle_ec::nat; +use bouncycastle_ec::sm2_scalar::{N_LIMBS, Sm2PublicScalar, Sm2Scalar, Sm2ScalarField}; + +const VALS_0: [u64; 4] = + [0x9faa8edecf05b151, 0x737180b1df95b1df, 0x5e15f338e5ade727, 0x99f78f956c6fcbdb]; +const VALS_1: [u64; 4] = + [0xa532d66b2b815019, 0x631e855da126849b, 0x509db3d2fe8976fd, 0x9621cdb0ea646d19]; +const VALS_2: [u64; 4] = + [0x0a849fb061ae1a7d, 0x69263174952f9abb, 0xabd3f5fdfb0dfaa2, 0xab1c24ac2d175095]; +const VALS_3: [u64; 4] = + [0xf4566b478fa51f10, 0x42170adca9ddf2aa, 0xa537081998f3fdb5, 0x5a4b600da638c009]; +const VALS_4: [u64; 4] = + [0x3f4c26b6409b4c74, 0x5e8741237b04a245, 0xa65df2b6a3eac017, 0xf6f405237452f7b9]; +const VALS_5: [u64; 4] = + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; + +const ADD_0_1: [u64; 4] = + [0xf1217140c0b1c047, 0x648c26a45ef6314f, 0xaeb3a70be4375e25, 0x30195d4756d438f4]; +const SUB_0_1: [u64; 4] = + [0xfa77b873a3846138, 0x1052fb543e6f2d43, 0x0d783f65e724702a, 0x03d5c1e4820b5ec2]; +const MUL_0_1: [u64; 4] = + [0x54cb882bb2dfde33, 0x3cdac7e002f2c830, 0x17fe944cc047de08, 0xe184473303a91459]; +const ADD_0_2: [u64; 4] = + [0x56733a85f6de8aab, 0x6a93d2bb52ff476f, 0x09e9e936e0bbe1ca, 0x4513b44299871c71]; +const SUB_0_2: [u64; 4] = + [0xe8e1e337a72cd7f7, 0x7c4f2ea86c2c1c4f, 0xb241fd3aea9fec84, 0xeedb6ae83f587b45]; +const MUL_0_2: [u64; 4] = + [0xce133b263051f295, 0x7f5b2d9353ea9817, 0x2978d2df1490a72e, 0x70113776167bd6dd]; +const ADD_0_3: [u64; 4] = + [0x9400fa265eaad061, 0xb5888b8e8973a48a, 0x034cfb527ea1e4dc, 0xf442efa312a88be5]; +const SUB_0_3: [u64; 4] = + [0xab5423973f609241, 0x315a75d535b7bf34, 0xb8deeb1f4cb9e972, 0x3fac2f87c6370bd1]; +const MUL_0_3: [u64; 4] = + [0x77ada45169fc56f9, 0x288058969837995a, 0xe4bcd09a9541d039, 0xbf67f4e573635ca4]; +const ADD_0_4: [u64; 4] = + [0x8b3ac18bd5cbbca2, 0x5ff4e26a38d44ef9, 0x0473e5ef8998a73f, 0x90eb94b9e0c2c395]; +const SUB_0_4: [u64; 4] = + [0xb41a5c31c83fa600, 0x86ee1ef9865714c5, 0xb7b8008241c3270f, 0xa3038a70f81cd421]; +const MUL_0_4: [u64; 4] = + [0xaa2eb00b9c2752d5, 0xa43cfb914ecdfcf8, 0xd794ba4aedb3c049, 0x0919562951b398cf]; +const ADD_0_5: [u64; 4] = + [0x9faa8edecf05b151, 0x737180b1df95b1df, 0x5e15f338e5ade727, 0x99f78f956c6fcbdb]; +const SUB_0_5: [u64; 4] = + [0x9faa8edecf05b151, 0x737180b1df95b1df, 0x5e15f338e5ade727, 0x99f78f956c6fcbdb]; +const MUL_0_5: [u64; 4] = + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; +const ADD_1_2: [u64; 4] = + [0x5bfb8212535a2973, 0x5a40d76714901a2b, 0xfc71a9d0f99771a0, 0x413df25e177bbdae]; +const SUB_1_2: [u64; 4] = + [0xee6a2ac403a876bf, 0x6bfc33542dbcef0b, 0xa4c9bdd5037b7c5a, 0xeb05a903bd4d1c83]; +const MUL_1_2: [u64; 4] = + [0x06211f16fe8ed084, 0x1f1baabebaab43c3, 0xe9cdb399af16bd7c, 0xf0f1114db3eb8930]; +const ADD_1_3: [u64; 4] = + [0x998941b2bb266f29, 0xa535903a4b047746, 0xf5d4bbec977d74b2, 0xf06d2dbe909d2d22]; +const SUB_1_3: [u64; 4] = + [0xb0dc6b239bdc3109, 0x21077a80f74891f0, 0xab66abb965957948, 0x3bd66da3442bad0f]; +const MUL_1_3: [u64; 4] = + [0x544725d8c7df5421, 0xf57246593505ede9, 0x6d9f55a3a04d7f64, 0xe4068115f983aa92]; +const ADD_1_4: [u64; 4] = + [0x90c3091832475b6a, 0x4fa1e715fa6521b5, 0xf6fba689a2743715, 0x8d15d2d55eb764d2]; +const SUB_1_4: [u64; 4] = + [0xb9a2a3be24bb44c8, 0x769b23a547e7e781, 0xaa3fc11c5a9eb6e5, 0x9f2dc88c7611755f]; +const MUL_1_4: [u64; 4] = + [0x13a0fedb78ae289b, 0xc4854ff70c07d775, 0xdd5522338712cc46, 0x02173884bed9fb54]; +const ADD_1_5: [u64; 4] = + [0xa532d66b2b815019, 0x631e855da126849b, 0x509db3d2fe8976fd, 0x9621cdb0ea646d19]; +const SUB_1_5: [u64; 4] = + [0xa532d66b2b815019, 0x631e855da126849b, 0x509db3d2fe8976fd, 0x9621cdb0ea646d19]; +const MUL_1_5: [u64; 4] = + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; +const ADD_2_3: [u64; 4] = + [0xab1f16eeb77df86a, 0x39395ce61d47883a, 0x510afe179401f858, 0x056784bad350109f]; +const SUB_2_3: [u64; 4] = + [0x162e3468d208fb6d, 0x270f2697eb51a810, 0x069cede46219fced, 0x50d0c49e86de908c]; +const MUL_2_3: [u64; 4] = + [0xd26214a5d839f771, 0xa0c0afc2633c9d1e, 0x6087bd93952d10d0, 0x0af1b9e483c6fb5b]; +const ADD_2_4: [u64; 4] = + [0xf614d25d687425ce, 0x55a9932cee6e37d4, 0x5231e8b49ef8baba, 0xa21029d0a16a484f]; +const SUB_2_4: [u64; 4] = + [0x1ef46d035ae80f2c, 0x7ca2cfbc3bf0fda1, 0x0576034757233a8a, 0xb4281f87b8c458dc]; +const MUL_2_4: [u64; 4] = + [0xe0cc8dd68a636466, 0xee4b443dd28b0dbd, 0x8966f3eeede019c2, 0x1bf819d9696a2c83]; +const ADD_2_5: [u64; 4] = + [0x0a849fb061ae1a7d, 0x69263174952f9abb, 0xabd3f5fdfb0dfaa2, 0xab1c24ac2d175095]; +const SUB_2_5: [u64; 4] = + [0x0a849fb061ae1a7d, 0x69263174952f9abb, 0xabd3f5fdfb0dfaa2, 0xab1c24ac2d175095]; +const MUL_2_5: [u64; 4] = + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; +const ADD_3_4: [u64; 4] = + [0xdfe69df4966b2a61, 0x2e9a6c95031c8fc4, 0x4b94fad03cdebdcd, 0x513f65321a8bb7c3]; +const SUB_3_4: [u64; 4] = + [0x08c6389a88df13bf, 0x5593a924509f5591, 0xfed91562f5093d9d, 0x63575ae931e5c84f]; +const MUL_3_4: [u64; 4] = + [0xc63c7d29a151abd6, 0xa6db469c668c47d2, 0xf8fb94f011c0703f, 0x27e44d445af9d9b0]; +const ADD_3_5: [u64; 4] = + [0xf4566b478fa51f10, 0x42170adca9ddf2aa, 0xa537081998f3fdb5, 0x5a4b600da638c009]; +const SUB_3_5: [u64; 4] = + [0xf4566b478fa51f10, 0x42170adca9ddf2aa, 0xa537081998f3fdb5, 0x5a4b600da638c009]; +const MUL_3_5: [u64; 4] = + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; +const ADD_4_5: [u64; 4] = + [0x3f4c26b6409b4c74, 0x5e8741237b04a245, 0xa65df2b6a3eac017, 0xf6f405237452f7b9]; +const SUB_4_5: [u64; 4] = + [0x3f4c26b6409b4c74, 0x5e8741237b04a245, 0xa65df2b6a3eac017, 0xf6f405237452f7b9]; +const MUL_4_5: [u64; 4] = + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; + +const NEG_0: [u64; 4] = + [0xb411652a6acf8fd2, 0xfe925eb94230534b, 0xa1ea0cc71a5218d7, 0x6608706993903424]; +const INV_0: [u64; 4] = + [0xefae022b331945ff, 0x633474b3ff271f73, 0x6bd2639865f9b139, 0xab1ae1671e8da2ee]; +const NEG_1: [u64; 4] = + [0xae891d9e0e53f10a, 0x0ee55a0d809f808f, 0xaf624c2d01768902, 0x69de324e159b92e6]; +const INV_1: [u64; 4] = + [0x40cea9ee7d9d71e2, 0x7d60f6fa1fc0f1c0, 0x3a7f4fca4e1523fa, 0xe71dbb4c556050ec]; +const NEG_2: [u64; 4] = + [0x49375458d82726a6, 0x08ddadf68c966a70, 0x542c0a0204f2055d, 0x54e3db52d2e8af6a]; +const INV_2: [u64; 4] = + [0x6bc83663ebfca2c2, 0xe203bf0c8aa60602, 0xa3ba8ea3f5edaa75, 0x673f78118100331a]; +const NEG_3: [u64; 4] = + [0x5f6588c1aa302213, 0x2fecd48e77e81280, 0x5ac8f7e6670c024a, 0xa5b49ff159c73ff6]; +const INV_3: [u64; 4] = + [0xf0cfb12c2f88d2d0, 0x22d44d518ea40181, 0x3c9a9ffc42c0f0e2, 0xebb4349fb0d891f7]; +const NEG_4: [u64; 4] = + [0x146fcd52f939f4af, 0x137c9e47a6c162e6, 0x59a20d495c153fe8, 0x090bfadb8bad0846]; +const INV_4: [u64; 4] = + [0xd042e6c194809037, 0x6b61a6a776549a7a, 0xe367516d6fe2c106, 0xd25dabfa3f2f3a4e]; +const NEG_5: [u64; 4] = + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; +const INV_5: [u64; 4] = + [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; + +fn fe(limbs: [u64; 4]) -> Sm2ScalarField { + Sm2ScalarField::from_limbs(limbs) +} + +#[test] +fn known_answer_pairwise_add_sub_mul() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5].map(fe); + let add: Vec> = vec![ + vec![ADD_0_1, ADD_0_2, ADD_0_3, ADD_0_4, ADD_0_5], + vec![ADD_1_2, ADD_1_3, ADD_1_4, ADD_1_5], + vec![ADD_2_3, ADD_2_4, ADD_2_5], + vec![ADD_3_4, ADD_3_5], + vec![ADD_4_5], + ]; + let sub: Vec> = vec![ + vec![SUB_0_1, SUB_0_2, SUB_0_3, SUB_0_4, SUB_0_5], + vec![SUB_1_2, SUB_1_3, SUB_1_4, SUB_1_5], + vec![SUB_2_3, SUB_2_4, SUB_2_5], + vec![SUB_3_4, SUB_3_5], + vec![SUB_4_5], + ]; + let mul: Vec> = vec![ + vec![MUL_0_1, MUL_0_2, MUL_0_3, MUL_0_4, MUL_0_5], + vec![MUL_1_2, MUL_1_3, MUL_1_4, MUL_1_5], + vec![MUL_2_3, MUL_2_4, MUL_2_5], + vec![MUL_3_4, MUL_3_5], + vec![MUL_4_5], + ]; + for i in 0..6 { + for j in (i + 1)..6 { + let expected_add = fe(add[i][j - i - 1]); + let expected_sub = fe(sub[i][j - i - 1]); + let expected_mul = fe(mul[i][j - i - 1]); + assert_eq!(vals[i].add(&vals[j]), expected_add, "add({i},{j})"); + assert_eq!(vals[j].add(&vals[i]), expected_add, "add is commutative ({j},{i})"); + assert_eq!(vals[i].sub(&vals[j]), expected_sub, "sub({i},{j})"); + assert_eq!(vals[i].mul(&vals[j]), expected_mul, "mul({i},{j})"); + assert_eq!(vals[j].mul(&vals[i]), expected_mul, "mul is commutative ({j},{i})"); + } + } +} + +#[test] +fn known_answer_negate_and_invert() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5]; + let negs = [NEG_0, NEG_1, NEG_2, NEG_3, NEG_4, NEG_5]; + let invs = [INV_0, INV_1, INV_2, INV_3, INV_4, INV_5]; + for i in 0..6 { + assert_eq!(fe(vals[i]).negate(), fe(negs[i]), "negate({i})"); + assert_eq!(fe(vals[i]).invert(), fe(invs[i]), "invert({i})"); + } +} + +#[test] +fn to_limbs_round_trips() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + assert_eq!(fe(limbs).to_limbs(), limbs); + } +} + +#[test] +fn identities() { + let vals = [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5].map(fe); + for &v in &vals { + assert_eq!(v.add(&Sm2ScalarField::ZERO), v, "x + 0 == x"); + assert_eq!(v.mul(&Sm2ScalarField::ONE), v, "x * 1 == x"); + assert_eq!(v.sub(&v), Sm2ScalarField::ZERO, "x - x == 0"); + assert_eq!(v.add(&v.negate()), Sm2ScalarField::ZERO, "x + (-x) == 0"); + if v != Sm2ScalarField::ZERO { + assert_eq!(v.mul(&v.invert()), Sm2ScalarField::ONE, "x * x^-1 == 1"); + } else { + assert_eq!(v.invert(), Sm2ScalarField::ZERO, "0^-1 == 0 by convention"); + } + } +} + +#[test] +fn eq_detects_a_difference_in_any_limb() { + let base = fe(VALS_0); + assert_eq!(base, fe(VALS_0)); + for limb_idx in 0..4 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!(base, fe(other), "a difference in limb {limb_idx} must be detected"); + } +} + +#[test] +fn from_limbs_reduces_out_of_range_input() { + assert_eq!(fe(N_LIMBS), Sm2ScalarField::ZERO); + let n_plus_1: [u64; 4] = + [0x53bbf40939d54124, 0x7203df6b21c6052b, 0xffffffffffffffff, 0xfffffffeffffffff]; + assert_eq!(fe(n_plus_1), Sm2ScalarField::ONE); +} + +#[test] +fn public_scalar_round_trips_and_reduces() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + assert_eq!(Sm2PublicScalar::from_limbs(limbs).to_limbs(), limbs); + } + assert_eq!(Sm2PublicScalar::from_limbs(N_LIMBS).to_limbs(), [0, 0, 0, 0]); + let n_plus_1: [u64; 4] = + [0x53bbf40939d54124, 0x7203df6b21c6052b, 0xffffffffffffffff, 0xfffffffeffffffff]; + assert_eq!(Sm2PublicScalar::from_limbs(n_plus_1).to_limbs(), [1, 0, 0, 0]); +} + +#[test] +fn public_scalar_eq_detects_a_difference_in_any_limb() { + let base = Sm2PublicScalar::from_limbs(VALS_0); + assert_eq!(base, Sm2PublicScalar::from_limbs(VALS_0)); + for limb_idx in 0..4 { + let mut other = VALS_0; + other[limb_idx] ^= 1; + assert_ne!( + base, + Sm2PublicScalar::from_limbs(other), + "a difference in limb {limb_idx} must be detected" + ); + } +} + +#[test] +fn secret_scalar_be_bytes_round_trip() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4] { + let secret = Sm2Scalar::from_limbs(limbs); + let round_tripped = Sm2Scalar::from_be_bytes(&secret.to_be_bytes()); + assert_eq!(round_tripped, secret); + } +} + +#[test] +fn secret_scalar_from_be_bytes_reduces_out_of_range_input() { + let mut n_bytes = [0u8; 32]; + for (i, limb) in N_LIMBS.iter().rev().enumerate() { + n_bytes[i * 8..i * 8 + 8].copy_from_slice(&limb.to_be_bytes()); + } + assert_eq!(Sm2Scalar::from_be_bytes(&n_bytes), Sm2Scalar::from_limbs([0, 0, 0, 0])); +} + +#[test] +fn scalar_field_from_secret_matches_from_limbs() { + for limbs in [VALS_0, VALS_1, VALS_2, VALS_3, VALS_4, VALS_5] { + let secret = Sm2Scalar::from_limbs(limbs); + assert_eq!(Sm2ScalarField::from_secret(&secret), fe(limbs)); + } +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property test. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn algebraic_identities_over_many_pseudorandom_values() { + let mut rng = Xorshift64(0x1917201720172017); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + let b = fe(rng.next_limbs()); + let c = fe(rng.next_limbs()); + + assert_eq!(a.add(&b), b.add(&a), "add is commutative"); + assert_eq!(a.mul(&b), b.mul(&a), "mul is commutative"); + assert_eq!(a.add(&b).add(&c), a.add(&b.add(&c)), "add is associative"); + assert_eq!(a.mul(&b).mul(&c), a.mul(&b.mul(&c)), "mul is associative"); + assert_eq!(a.mul(&b.add(&c)), a.mul(&b).add(&a.mul(&c)), "mul distributes over add"); + assert_eq!(a.sub(&b).add(&b), a, "(a - b) + b == a"); + assert_eq!(a.add(&b).sub(&b), a, "(a + b) - b == a"); + if a != Sm2ScalarField::ZERO { + assert_eq!(a.mul(&a.invert()), Sm2ScalarField::ONE, "a * a^-1 == 1"); + } + } +} + +/// `square` is a different routine from `mul`, not a wrapper around it (see the field module's +/// `widening_square`), so the property that makes it correct -- agreeing with `mul` on every +/// input -- is worth pinning directly rather than only through `invert`, which is the only +/// caller that would otherwise exercise it. Includes the values most likely to expose a carry +/// bug in the doubling or diagonal passes: zero, one, and all-ones limbs. +#[test] +fn square_agrees_with_mul() { + let mut rng = Xorshift64(0xC0FFEE000000000B); + for _ in 0..5000 { + let a = fe(rng.next_limbs()); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul"); + } + for limbs in [ + [0u64; 4], + { + let mut l = [0u64; 4]; + l[0] = 1; + l + }, + [u64::MAX; 4], + { + let mut l = [u64::MAX; 4]; + l[0] = 0; + l + }, + ] { + let a = fe(limbs); + assert_eq!(a.square(), a.mul(&a), "square disagrees with mul for {limbs:x?}"); + } +} + +/// `zeroize` is the signing path's way of not leaving `d`, `k` or `k^-1` legible on the stack +/// (see the method's own docs for why this type is not wrapped in `Secret` instead). Whether the +/// volatile write survives optimization cannot be observed from Rust; that the value is actually +/// cleared can be, and is what would break if the method were ever reduced to a no-op. +#[test] +fn zeroize_clears_the_value() { + let mut a = fe([1, 2, 3, 4]); + assert_ne!(a, Sm2ScalarField::ZERO, "precondition: the value starts non-zero"); + a.zeroize(); + assert_eq!(a, Sm2ScalarField::ZERO, "zeroize must leave the value at zero"); + + // and it is idempotent, so a caller scrubbing twice on overlapping paths is harmless + a.zeroize(); + assert_eq!(a, Sm2ScalarField::ZERO); +} + +/// `is_zero` on the scalar field, pinned for both truth values and across every limb position +/// (see the field test of the same name for why); it is the check behind `negate`'s `0 -> 0` +/// special case. +#[test] +fn is_zero_distinguishes_zero_from_every_nonzero_limb_position() { + assert!(Sm2ScalarField::ZERO.is_zero().to_bool()); + assert!(fe(bouncycastle_ec::sm2_scalar::N_LIMBS).is_zero().to_bool(), "n reduces to 0"); + assert!(!Sm2ScalarField::ONE.is_zero().to_bool()); + for limb_idx in 0..4 { + let mut limbs = [0u64; 4]; + limbs[limb_idx] = 1; + assert!(!fe(limbs).is_zero().to_bool(), "a set bit in limb {limb_idx} must be seen"); + } +} + +/// `n - 1`, the largest canonical scalar, is the worst case for REDC's final conditional +/// subtraction and for `add`'s carry correction, and was absent from every known-answer set +/// above (whose values are pseudorandom, so never near `n`). Expected values follow from +/// `(n-1)^2 == 1` and `(n-1) + (n-1) == n - 2` in the field, computed from `n` in Python. +#[test] +fn known_answer_at_n_minus_1() { + let n_minus_1_limbs: [u64; 4] = + [0x53bbf40939d54122, 0x7203df6b21c6052b, 0xffffffffffffffff, 0xfffffffeffffffff]; + let n_minus_1 = fe(n_minus_1_limbs); + let n_minus_2 = + fe([0x53bbf40939d54121, 0x7203df6b21c6052b, 0xffffffffffffffff, 0xfffffffeffffffff]); + + assert_eq!(n_minus_1.to_limbs(), n_minus_1_limbs, "Montgomery round trip of n-1"); + assert_eq!(Sm2PublicScalar::from_limbs(n_minus_1_limbs).to_limbs(), n_minus_1_limbs); + assert_eq!(n_minus_1.add(&n_minus_1), n_minus_2, "(n-1) + (n-1) == n-2"); + assert_eq!(n_minus_1.add(&Sm2ScalarField::ONE), Sm2ScalarField::ZERO, "(n-1) + 1 == 0"); + assert_eq!(Sm2ScalarField::ZERO.sub(&Sm2ScalarField::ONE), n_minus_1, "0 - 1 == n-1"); + assert_eq!(n_minus_1.mul(&n_minus_1), Sm2ScalarField::ONE, "(n-1)^2 == 1"); + assert_eq!(n_minus_1.square(), Sm2ScalarField::ONE, "(n-1)^2 == 1, via square"); + assert_eq!(n_minus_1.negate(), Sm2ScalarField::ONE, "-(n-1) == 1"); + assert_eq!(n_minus_1.invert(), n_minus_1, "(n-1)^-1 == n-1"); + assert_eq!(Sm2ScalarField::ONE.invert(), Sm2ScalarField::ONE, "1^-1 == 1"); +} + +/// Products whose REDC pre-reduction value lands in `[n, 2^256)` -- `acc[8] = 0` and `high >= n` +/// -- so the final conditional subtraction is the step that produces the canonical result. That +/// needs the product's canonical Montgomery representative, `abR mod n` (what REDC returns; the +/// `ab mod n` the test compares is 254 to 256 bits here), below `2^256 - n`, about `2^224`, and +/// the wrong representative drawn, `2^-32` per reduction, so no pseudorandom or round-trip test +/// reaches it; these were constructed, not searched (pick the Montgomery operand `a` and the +/// target `u`, solve `m = uR * n^-1 mod a`, `b = (uR - mn) / a`). A plain +/// `mul(..) == from_limbs(expected)` would NOT detect the subtraction's absence: `from_limbs` of +/// the expected value takes the same step and would land on the same non-canonical +/// representative. The two extra assertions are what pin it. +#[test] +fn known_answer_mul_reaching_the_final_subtraction() { + let cases: [([u64; 4], [u64; 4], [u64; 4]); 3] = [ + ( + [0xcc0763d7a2a61e2e, 0xe3e7e0e51c470ea0, 0x64d4afaca15a0fe5, 0xd39e2a14c5fad104], + [0x6fed21e2d993738d, 0xb256903f893f641e, 0x800c9d58bd0ed69d, 0xb3abdcfe03d26162], + [0xf7632108662761bb, 0x2ea1b6cc5a05df86, 0xf85446494d6ec949, 0x61814d697467fe93], + ), + ( + [0x220bc6677cac3348, 0xbd56b3bef29ea6cf, 0xbc548b1d35fb72ca, 0x5436dcde311c8535], + [0x5fa32a181cc2adc6, 0x990944d795a06fd1, 0x256b1f9f049ff59a, 0x5c49581b3bcf99da], + [0xf7bb16069a9fbdd1, 0xa9b6720591dff91a, 0xcfb254d884a3a3df, 0x94aa57d7cb811c82], + ), + ( + [0x91bafd07c6d8e402, 0x150e9d662c62dc0d, 0x0f9c4d1c16d2c93f, 0x52ac4d55133a8232], + [0xfba938138feff62d, 0xacbf42b139fd6682, 0xf78407c4c7c96c45, 0x521b875c9611d4fd], + [0xd6f27669b7a11bd9, 0x6c88bca9c0b127c4, 0x2f33d68c2e78a389, 0x2e4612ca50df6aac], + ), + ]; + for (x, y, expected) in cases { + for (a, b) in [(x, y), (y, x)] { + let prod = fe(a).mul(&fe(b)); + assert_eq!(prod.to_limbs(), expected, "a = {a:x?}"); + // The output must be canonical: unchanged by adding zero, and negating to n - expected. + assert_eq!(prod, prod.add(&Sm2ScalarField::ZERO), "non-canonical output, a = {a:x?}"); + let (n_minus_expected, _) = nat::sub(&N_LIMBS, &expected); + assert_eq!(prod.negate().to_limbs(), n_minus_expected, "a = {a:x?}"); + } + } +} diff --git a/crypto/ec/tests/sm2_sec1_tests.rs b/crypto/ec/tests/sm2_sec1_tests.rs new file mode 100644 index 000000000..dcca1d6de --- /dev/null +++ b/crypto/ec/tests/sm2_sec1_tests.rs @@ -0,0 +1,134 @@ +//! Known-answer tests for SEC 1 §2.3.3/§2.3.4 point encoding and decoding, SM2. +//! +//! `G`'s coordinates are `draft-shen-sm2-ecdsa-02` Appendix D's (already verified independently in +//! `sm2_point_tests.rs`); `2G`'s were computed independently in Python via the standard `a = -3` +//! affine group law, specifically because it has odd `y` where `G`'s is even, so between them both +//! compressed tags (`0x02`/`0x03`) are exercised. The encodings below were derived from those +//! coordinates independently in Python via plain big-endian byte packing. + +use bouncycastle_ec::sm2::Sm2FieldElement; +use bouncycastle_ec::sm2_domain::{G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::sm2_sec1::{decode, decode_point, encode_compressed, encode_uncompressed}; + +const TWO_G_X: [u64; 4] = + [0x495c2e1da3f2bd52, 0x9c0dfa08c08a7331, 0x0d58ef57fa73ba4d, 0x56cefd60d7c87c00]; +const TWO_G_Y: [u64; 4] = + [0x6f780d3a970a23c3, 0x6de84c182f6c8e71, 0x68535ce0f8eaf1bd, 0x31b7e7e6cc8189f6]; + +const UNCOMPRESSED_G: [u8; 65] = [ + 0x04, 0x32, 0xc4, 0xae, 0x2c, 0x1f, 0x19, 0x81, 0x19, 0x5f, 0x99, 0x04, 0x46, 0x6a, 0x39, 0xc9, + 0x94, 0x8f, 0xe3, 0x0b, 0xbf, 0xf2, 0x66, 0x0b, 0xe1, 0x71, 0x5a, 0x45, 0x89, 0x33, 0x4c, 0x74, + 0xc7, 0xbc, 0x37, 0x36, 0xa2, 0xf4, 0xf6, 0x77, 0x9c, 0x59, 0xbd, 0xce, 0xe3, 0x6b, 0x69, 0x21, + 0x53, 0xd0, 0xa9, 0x87, 0x7c, 0xc6, 0x2a, 0x47, 0x40, 0x02, 0xdf, 0x32, 0xe5, 0x21, 0x39, 0xf0, + 0xa0, +]; + +const COMPRESSED_G: [u8; 33] = [ + 0x02, 0x32, 0xc4, 0xae, 0x2c, 0x1f, 0x19, 0x81, 0x19, 0x5f, 0x99, 0x04, 0x46, 0x6a, 0x39, 0xc9, + 0x94, 0x8f, 0xe3, 0x0b, 0xbf, 0xf2, 0x66, 0x0b, 0xe1, 0x71, 0x5a, 0x45, 0x89, 0x33, 0x4c, 0x74, + 0xc7, +]; + +const UNCOMPRESSED_TWO_G: [u8; 65] = [ + 0x04, 0x56, 0xce, 0xfd, 0x60, 0xd7, 0xc8, 0x7c, 0x00, 0x0d, 0x58, 0xef, 0x57, 0xfa, 0x73, 0xba, + 0x4d, 0x9c, 0x0d, 0xfa, 0x08, 0xc0, 0x8a, 0x73, 0x31, 0x49, 0x5c, 0x2e, 0x1d, 0xa3, 0xf2, 0xbd, + 0x52, 0x31, 0xb7, 0xe7, 0xe6, 0xcc, 0x81, 0x89, 0xf6, 0x68, 0x53, 0x5c, 0xe0, 0xf8, 0xea, 0xf1, + 0xbd, 0x6d, 0xe8, 0x4c, 0x18, 0x2f, 0x6c, 0x8e, 0x71, 0x6f, 0x78, 0x0d, 0x3a, 0x97, 0x0a, 0x23, + 0xc3, +]; + +const COMPRESSED_TWO_G: [u8; 33] = [ + 0x03, 0x56, 0xce, 0xfd, 0x60, 0xd7, 0xc8, 0x7c, 0x00, 0x0d, 0x58, 0xef, 0x57, 0xfa, 0x73, 0xba, + 0x4d, 0x9c, 0x0d, 0xfa, 0x08, 0xc0, 0x8a, 0x73, 0x31, 0x49, 0x5c, 0x2e, 0x1d, 0xa3, 0xf2, 0xbd, + 0x52, +]; + +fn fe(limbs: [u64; 4]) -> Sm2FieldElement { + Sm2FieldElement::from_limbs(limbs) +} + +#[test] +fn known_answer_encode_uncompressed() { + assert_eq!(encode_uncompressed(&fe(G_X_LIMBS), &fe(G_Y_LIMBS)), UNCOMPRESSED_G); + assert_eq!(encode_uncompressed(&fe(TWO_G_X), &fe(TWO_G_Y)), UNCOMPRESSED_TWO_G); +} + +#[test] +fn known_answer_encode_compressed() { + assert_eq!( + encode_compressed(&fe(G_X_LIMBS), &fe(G_Y_LIMBS)), + COMPRESSED_G, + "G has even y -> tag 0x02" + ); + assert_eq!( + encode_compressed(&fe(TWO_G_X), &fe(TWO_G_Y)), + COMPRESSED_TWO_G, + "2G has odd y -> tag 0x03" + ); +} + +#[test] +fn known_answer_decode_uncompressed() { + let (x, y) = decode(&UNCOMPRESSED_G).expect("valid uncompressed point"); + assert_eq!(x, fe(G_X_LIMBS)); + assert_eq!(y, fe(G_Y_LIMBS)); +} + +#[test] +fn known_answer_decode_compressed() { + let (x, y) = decode(&COMPRESSED_G).expect("valid compressed point, tag 0x02"); + assert_eq!(x, fe(G_X_LIMBS)); + assert_eq!(y, fe(G_Y_LIMBS)); + + let (x2, y2) = decode(&COMPRESSED_TWO_G).expect("valid compressed point, tag 0x03"); + assert_eq!(x2, fe(TWO_G_X)); + assert_eq!(y2, fe(TWO_G_Y)); +} + +#[test] +fn round_trip_uncompressed_and_compressed() { + for (x, y) in [(G_X_LIMBS, G_Y_LIMBS), (TWO_G_X, TWO_G_Y)] { + let uncompressed = encode_uncompressed(&fe(x), &fe(y)); + assert_eq!(decode(&uncompressed), Some((fe(x), fe(y)))); + + let compressed = encode_compressed(&fe(x), &fe(y)); + assert_eq!(decode(&compressed), Some((fe(x), fe(y)))); + + assert!(decode_point(&uncompressed).unwrap().to_affine().is_some()); + } +} + +#[test] +fn decode_rejects_malformed_input() { + assert_eq!(decode(&[]), None); + assert_eq!(decode(&[0x04; 64]), None); + assert_eq!(decode(&[0x04; 66]), None); + assert_eq!(decode(&[0x02; 16]), None); + + let mut bad_tag_65 = UNCOMPRESSED_G; + bad_tag_65[0] = 0x02; + assert_eq!(decode(&bad_tag_65), None); + + let mut bad_tag_33 = COMPRESSED_G; + bad_tag_33[0] = 0x04; + assert_eq!(decode(&bad_tag_33), None); + + assert_eq!(decode(&[0x00]), None); + + let mut x_too_big = COMPRESSED_G; + x_too_big[1..33].fill(0xff); + assert_eq!(decode(&x_too_big), None); + + let mut no_root = COMPRESSED_G; + no_root[1..33].fill(0); + no_root[32] = 2; // x = 2, confirmed (in the Python derivation) to have no square root + assert_eq!(decode(&no_root), None); + + let mut not_on_curve = UNCOMPRESSED_G; + not_on_curve[64] ^= 0x01; + assert_eq!(decode(¬_on_curve), None); + + let mut y_too_big = UNCOMPRESSED_G; + y_too_big[33..65].fill(0xff); + assert_eq!(decode(&y_too_big), None); +} diff --git a/crypto/ec/tests/sm2_wnaf_tests.rs b/crypto/ec/tests/sm2_wnaf_tests.rs new file mode 100644 index 000000000..0b4277b81 --- /dev/null +++ b/crypto/ec/tests/sm2_wnaf_tests.rs @@ -0,0 +1,147 @@ +//! Known-answer tests for [`shamir_multiply`] (`[u]G + [v]Q`), SM2. +//! +//! `Q` below is `d*G` for a fixed test-only `d` (not a real key). Expected results were computed +//! independently in Python from the affine group law (`a = -3`), the same way as +//! `sm2_comb_tests.rs`, with `random.seed(808917)`. + +use bouncycastle_ec::sm2::Sm2FieldElement; +use bouncycastle_ec::sm2_comb::comb_multiply_base_point; +use bouncycastle_ec::sm2_point::Sm2JacobianPoint; +use bouncycastle_ec::sm2_scalar::{Sm2PublicScalar, Sm2Scalar}; +use bouncycastle_ec::sm2_wnaf::shamir_multiply; + +const Q_X: [u64; 4] = + [0x30e4445e9ced5d35, 0x9403c12a2c8e5f2b, 0x066493e2dd175d35, 0x414278ad63916c59]; +const Q_Y: [u64; 4] = + [0xe1d32f6895b01fba, 0x15a5930808e4ebca, 0xe8c91327428a11ef, 0x8b8381a3b29ecdd5]; + +fn q() -> Sm2JacobianPoint { + Sm2JacobianPoint::from_affine( + Sm2FieldElement::from_limbs(Q_X), + Sm2FieldElement::from_limbs(Q_Y), + ) +} + +fn scalar(limbs: [u64; 4]) -> Sm2PublicScalar { + Sm2PublicScalar::from_limbs(limbs) +} + +fn assert_result_is(u: [u64; 4], v: [u64; 4], expected_x: [u64; 4], expected_y: [u64; 4]) { + let result = shamir_multiply(&scalar(u), &scalar(v), &q()); + let (rx, ry) = result.to_affine().expect("expected a non-infinity result"); + assert_eq!(rx, Sm2FieldElement::from_limbs(expected_x), "x mismatch for u={u:x?} v={v:x?}"); + assert_eq!(ry, Sm2FieldElement::from_limbs(expected_y), "y mismatch for u={u:x?} v={v:x?}"); +} + +#[test] +fn known_answer_large_u_v() { + let u: [u64; 4] = + [0xb6354f7dc5327e8c, 0x7066452f6836347d, 0x6eb944867e5078ec, 0xa421b5e9b502718b]; + let v: [u64; 4] = + [0x9e5ca34c198b2335, 0x613ec89b206a2e3e, 0x6694fd0186e14175, 0x79760b91eea092e0]; + assert_result_is( + u, + v, + [0x6a7b40d34ed83d66, 0xb68575756e80961c, 0x535bf7db19e8c493, 0xeb628a48ea4cb0b2], + [0xa30c82d2cf878375, 0x3178dbe8b7ceb98d, 0x874841d289fe9ec2, 0xd510a3dac4e0766d], + ); +} + +#[test] +fn edge_case_v_is_zero_reduces_to_u_times_g() { + assert_result_is( + [5, 0, 0, 0], + [0, 0, 0, 0], + [0xa575da57cc372a9e, 0x344a417b7fce19db, 0x040e008fdd5eb77a, 0xc749061668652e26], + [0xa6976eff5fbe6480, 0x5006206eb579ff7d, 0x4504c622b51cf38f, 0xf2df5db2d144e945], + ); +} + +#[test] +fn edge_case_u_is_zero_reduces_to_v_times_q() { + assert_result_is( + [0, 0, 0, 0], + [5, 0, 0, 0], + [0x6eef35f83dd36eba, 0x5c06a43c93b48594, 0xce7cb7da9c4e7fad, 0x6c2c5adb6dff6097], + [0xbc55a2835f29e68c, 0xfb2b16c31bc2a758, 0x47a41c4ed446f5d7, 0xe6ad613ed91ba836], + ); +} + +#[test] +fn edge_case_both_zero_is_infinity() { + let result = shamir_multiply(&scalar([0; 4]), &scalar([0; 4]), &q()); + assert!(result.to_affine().is_none()); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_limbs(&mut self) -> [u64; 4] { + [self.next_u64(), self.next_u64(), self.next_u64(), self.next_u64()] + } +} + +#[test] +fn cross_checked_against_the_comb_multiplier_over_many_pseudorandom_scalars() { + // shamir_multiply(k, 0, anything) must equal comb_multiply_base_point(k): two independently + // implemented multipliers (fixed-base comb vs. interleaved wNAF) computing the same [k]G. + let mut rng = Xorshift64(0xE7C558A6A6E7F1DE); + for _ in 0..500 { + let k_limbs = rng.next_limbs(); + let via_comb = comb_multiply_base_point(&Sm2Scalar::from_limbs(k_limbs)); + let via_wnaf = + shamir_multiply(&Sm2PublicScalar::from_limbs(k_limbs), &scalar([0; 4]), &q()); + assert_eq!(via_comb.to_affine(), via_wnaf.to_affine(), "k = {k_limbs:x?}"); + } +} + +/// Every other test in this file uses one fixed `Q = dG`. The three `Q` values here each take a +/// path that one cannot: `Q = infinity` makes every `[v]Q` digit an addition of the identity, +/// `Q = G` makes the two precomputed odd-multiple tables identical (so the interleaved +/// additions hit the same-point case of `add_vartime`), and `Q = -G` makes them negatives of +/// each other (the opposite-point case, and with `u == v` a final result of infinity). Expected +/// values come from the fixed-base comb multiplier on the combined scalar, which is pinned by +/// its own known-answer tests. +#[test] +fn edge_case_q_is_infinity_g_or_minus_g() { + let g = Sm2JacobianPoint::from_affine( + Sm2FieldElement::from_limbs(bouncycastle_ec::sm2_domain::G_X_LIMBS), + Sm2FieldElement::from_limbs(bouncycastle_ec::sm2_domain::G_Y_LIMBS), + ); + let mut u = [0u64; 4]; + u[0] = 12345; + let mut v = [0u64; 4]; + v[0] = 6789; + let mut u_plus_v = [0u64; 4]; + u_plus_v[0] = 19134; + let mut u_minus_v = [0u64; 4]; + u_minus_v[0] = 5556; + let via_comb = |k: [u64; 4]| comb_multiply_base_point(&Sm2Scalar::from_limbs(k)).to_affine(); + + // [u]G + [v]infinity == [u]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &Sm2JacobianPoint::INFINITY).to_affine(), + via_comb(u) + ); + // [u]G + [v]G == [u+v]G + assert_eq!(shamir_multiply(&scalar(u), &scalar(v), &g).to_affine(), via_comb(u_plus_v)); + // [u]G + [v](-G) == [u-v]G + assert_eq!( + shamir_multiply(&scalar(u), &scalar(v), &g.negate()).to_affine(), + via_comb(u_minus_v) + ); + // [u]G + [u](-G) == infinity + assert!(shamir_multiply(&scalar(u), &scalar(u), &g.negate()).is_infinity().to_bool()); + // [0]G + [v]G == [v]G: the G table is never consulted, only Q's + assert_eq!(shamir_multiply(&scalar([0u64; 4]), &scalar(v), &g).to_affine(), via_comb(v)); +} diff --git a/crypto/ecdsa/Cargo.toml b/crypto/ecdsa/Cargo.toml new file mode 100644 index 000000000..c0c65da62 --- /dev/null +++ b/crypto/ecdsa/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "bouncycastle-ecdsa" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-ec.workspace = true +bouncycastle-hmac.workspace = true +bouncycastle-rng.workspace = true +bouncycastle-sha2.workspace = true +bouncycastle-utils.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +criterion.workspace = true +serde_json = "1.0" + +[[bench]] +name = "ecdsa_p256_benches" +harness = false + +[[bench]] +name = "ecdsa_p384_benches" +harness = false + +[[bench]] +name = "ecdsa_p521_benches" +harness = false + +[[bench]] +name = "ecdsa_p256k1_benches" +harness = false + +[[bench]] +name = "ecdsa_bp256r1_benches" +harness = false + +[[bench]] +name = "ecdsa_bp384r1_benches" +harness = false + +[[bench]] +name = "ecdsa_bp512r1_benches" +harness = false diff --git a/crypto/ecdsa/benches/ecdsa_bp256r1_benches.rs b/crypto/ecdsa/benches/ecdsa_bp256r1_benches.rs new file mode 100644 index 000000000..d23464c28 --- /dev/null +++ b/crypto/ecdsa/benches/ecdsa_bp256r1_benches.rs @@ -0,0 +1,42 @@ +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_rng::DefaultRNG; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ecdsa::keys_bp256r1::keygen; + +const MSG: &[u8] = b"a representative message for benchmarking ECDSA brainpoolP256r1"; + +fn bench_ecdsa_bp256r1(c: &mut Criterion) { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + let mut group = c.benchmark_group("ecdsa_bp256r1"); + group.bench_function("keygen", |bencher| { + bencher.iter(|| black_box(keygen(&mut DefaultRNG::default()).unwrap())) + }); + group.bench_function("sign", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_rng", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_deterministic", |bencher| { + bencher.iter(|| black_box(black_box(&sk).sign_deterministic(black_box(MSG), None).unwrap())) + }); + group.bench_function("verify", |bencher| { + bencher.iter(|| black_box(&pk).verify(black_box(MSG), None, black_box(&sig)).unwrap()) + }); + group.finish(); +} + +criterion_group!(benches, bench_ecdsa_bp256r1); +criterion_main!(benches); diff --git a/crypto/ecdsa/benches/ecdsa_bp384r1_benches.rs b/crypto/ecdsa/benches/ecdsa_bp384r1_benches.rs new file mode 100644 index 000000000..6e6b84b2b --- /dev/null +++ b/crypto/ecdsa/benches/ecdsa_bp384r1_benches.rs @@ -0,0 +1,42 @@ +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_rng::DefaultRNG; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ecdsa::keys_bp384r1::keygen; + +const MSG: &[u8] = b"a representative message for benchmarking ECDSA brainpoolP384r1"; + +fn bench_ecdsa_bp384r1(c: &mut Criterion) { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + let mut group = c.benchmark_group("ecdsa_bp384r1"); + group.bench_function("keygen", |bencher| { + bencher.iter(|| black_box(keygen(&mut DefaultRNG::default()).unwrap())) + }); + group.bench_function("sign", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_rng", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_deterministic", |bencher| { + bencher.iter(|| black_box(black_box(&sk).sign_deterministic(black_box(MSG), None).unwrap())) + }); + group.bench_function("verify", |bencher| { + bencher.iter(|| black_box(&pk).verify(black_box(MSG), None, black_box(&sig)).unwrap()) + }); + group.finish(); +} + +criterion_group!(benches, bench_ecdsa_bp384r1); +criterion_main!(benches); diff --git a/crypto/ecdsa/benches/ecdsa_bp512r1_benches.rs b/crypto/ecdsa/benches/ecdsa_bp512r1_benches.rs new file mode 100644 index 000000000..5d84e6dd8 --- /dev/null +++ b/crypto/ecdsa/benches/ecdsa_bp512r1_benches.rs @@ -0,0 +1,42 @@ +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_rng::DefaultRNG; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ecdsa::keys_bp512r1::keygen; + +const MSG: &[u8] = b"a representative message for benchmarking ECDSA brainpoolP512r1"; + +fn bench_ecdsa_bp512r1(c: &mut Criterion) { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + let mut group = c.benchmark_group("ecdsa_bp512r1"); + group.bench_function("keygen", |bencher| { + bencher.iter(|| black_box(keygen(&mut DefaultRNG::default()).unwrap())) + }); + group.bench_function("sign", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_rng", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_deterministic", |bencher| { + bencher.iter(|| black_box(black_box(&sk).sign_deterministic(black_box(MSG), None).unwrap())) + }); + group.bench_function("verify", |bencher| { + bencher.iter(|| black_box(&pk).verify(black_box(MSG), None, black_box(&sig)).unwrap()) + }); + group.finish(); +} + +criterion_group!(benches, bench_ecdsa_bp512r1); +criterion_main!(benches); diff --git a/crypto/ecdsa/benches/ecdsa_p256_benches.rs b/crypto/ecdsa/benches/ecdsa_p256_benches.rs new file mode 100644 index 000000000..c41846c4b --- /dev/null +++ b/crypto/ecdsa/benches/ecdsa_p256_benches.rs @@ -0,0 +1,42 @@ +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_rng::DefaultRNG; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ecdsa::keys_p256::keygen; + +const MSG: &[u8] = b"a representative message for benchmarking ECDSA P-256"; + +fn bench_ecdsa_p256(c: &mut Criterion) { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + let mut group = c.benchmark_group("ecdsa_p256"); + group.bench_function("keygen", |bencher| { + bencher.iter(|| black_box(keygen(&mut DefaultRNG::default()).unwrap())) + }); + group.bench_function("sign", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_rng", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_deterministic", |bencher| { + bencher.iter(|| black_box(black_box(&sk).sign_deterministic(black_box(MSG), None).unwrap())) + }); + group.bench_function("verify", |bencher| { + bencher.iter(|| black_box(&pk).verify(black_box(MSG), None, black_box(&sig)).unwrap()) + }); + group.finish(); +} + +criterion_group!(benches, bench_ecdsa_p256); +criterion_main!(benches); diff --git a/crypto/ecdsa/benches/ecdsa_p256k1_benches.rs b/crypto/ecdsa/benches/ecdsa_p256k1_benches.rs new file mode 100644 index 000000000..9cf15470a --- /dev/null +++ b/crypto/ecdsa/benches/ecdsa_p256k1_benches.rs @@ -0,0 +1,42 @@ +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_rng::DefaultRNG; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ecdsa::keys_p256k1::keygen; + +const MSG: &[u8] = b"a representative message for benchmarking ECDSA secp256k1"; + +fn bench_ecdsa_secp256k1(c: &mut Criterion) { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + let mut group = c.benchmark_group("ecdsa_secp256k1"); + group.bench_function("keygen", |bencher| { + bencher.iter(|| black_box(keygen(&mut DefaultRNG::default()).unwrap())) + }); + group.bench_function("sign", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_rng", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_deterministic", |bencher| { + bencher.iter(|| black_box(black_box(&sk).sign_deterministic(black_box(MSG), None).unwrap())) + }); + group.bench_function("verify", |bencher| { + bencher.iter(|| black_box(&pk).verify(black_box(MSG), None, black_box(&sig)).unwrap()) + }); + group.finish(); +} + +criterion_group!(benches, bench_ecdsa_secp256k1); +criterion_main!(benches); diff --git a/crypto/ecdsa/benches/ecdsa_p384_benches.rs b/crypto/ecdsa/benches/ecdsa_p384_benches.rs new file mode 100644 index 000000000..23943c310 --- /dev/null +++ b/crypto/ecdsa/benches/ecdsa_p384_benches.rs @@ -0,0 +1,42 @@ +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_rng::DefaultRNG; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ecdsa::keys_p384::keygen; + +const MSG: &[u8] = b"a representative message for benchmarking ECDSA P-384"; + +fn bench_ecdsa_p384(c: &mut Criterion) { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + let mut group = c.benchmark_group("ecdsa_p384"); + group.bench_function("keygen", |bencher| { + bencher.iter(|| black_box(keygen(&mut DefaultRNG::default()).unwrap())) + }); + group.bench_function("sign", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_rng", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_deterministic", |bencher| { + bencher.iter(|| black_box(black_box(&sk).sign_deterministic(black_box(MSG), None).unwrap())) + }); + group.bench_function("verify", |bencher| { + bencher.iter(|| black_box(&pk).verify(black_box(MSG), None, black_box(&sig)).unwrap()) + }); + group.finish(); +} + +criterion_group!(benches, bench_ecdsa_p384); +criterion_main!(benches); diff --git a/crypto/ecdsa/benches/ecdsa_p521_benches.rs b/crypto/ecdsa/benches/ecdsa_p521_benches.rs new file mode 100644 index 000000000..cd1f1ba70 --- /dev/null +++ b/crypto/ecdsa/benches/ecdsa_p521_benches.rs @@ -0,0 +1,42 @@ +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_rng::DefaultRNG; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ecdsa::keys_p521::keygen; + +const MSG: &[u8] = b"a representative message for benchmarking ECDSA P-521"; + +fn bench_ecdsa_p521(c: &mut Criterion) { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + let mut group = c.benchmark_group("ecdsa_p521"); + group.bench_function("keygen", |bencher| { + bencher.iter(|| black_box(keygen(&mut DefaultRNG::default()).unwrap())) + }); + group.bench_function("sign", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_rng", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk).sign(&mut DefaultRNG::default(), black_box(MSG), None).unwrap(), + ) + }) + }); + group.bench_function("sign_deterministic", |bencher| { + bencher.iter(|| black_box(black_box(&sk).sign_deterministic(black_box(MSG), None).unwrap())) + }); + group.bench_function("verify", |bencher| { + bencher.iter(|| black_box(&pk).verify(black_box(MSG), None, black_box(&sig)).unwrap()) + }); + group.finish(); +} + +criterion_group!(benches, bench_ecdsa_p521); +criterion_main!(benches); diff --git a/crypto/ecdsa/src/der.rs b/crypto/ecdsa/src/der.rs new file mode 100644 index 000000000..a0c3bf993 --- /dev/null +++ b/crypto/ecdsa/src/der.rs @@ -0,0 +1,182 @@ +//! Minimal DER encode/decode for `Ecdsa-Sig-Value ::= SEQUENCE { r INTEGER, s INTEGER }` (RFC 3279 +//! §2.2.3), as an alternative to this crate's native raw `r || s` signature encoding (see e.g. +//! [`crate::ecdsa_p256::SIG_LEN`]'s docs on why raw was chosen as the default). This handles +//! exactly two ASN.1 types -- `INTEGER` (universal tag `0x02`) and constructed `SEQUENCE` +//! (universal tag `0x30`) -- which is all `Ecdsa-Sig-Value` ever needs, so there is no general +//! ASN.1/BER/DER crate here, just this. +//! +//! Every rule enforced below is from ITU-T Rec. X.690 (02/2021), "ASN.1 encoding rules: +//! Specification of Basic Encoding Rules (BER), Canonical Encoding Rules (CER) and Distinguished +//! Encoding Rules (DER)": +//! +//! - **Length octets** (§8.1.3): short form (a single octet, bit 8 clear, encoding 0-127 directly) +//! or long form (an initial octet with bit 8 set and bits 7-1 giving the count of subsequent +//! octets, which then big-endian-encode the length). [`max_len`]'s bound (66-byte `r`/`s`, the +//! widest this crate supports, for P-521) never needs more than one subsequent length octet, so +//! [`parse_length`]/[`write_length`] don't implement the general long form, only that one case. +//! - **DER's length restriction** (§10.1): "the minimum number of octets" -- so a long-form length +//! encoding a value `< 128` (which should have used the short form) is rejected, and the +//! indefinite form (§8.1.3.6, a single `0x80` octet) is not a definite length at all and is +//! rejected outright. +//! - **INTEGER content** (§8.3.2/§8.3.3): a two's-complement binary number, minimally encoded -- +//! §8.3.2 forbids a leading octet (plus bit 8 of the next) that is "all ones" or "all zero", +//! since either means a redundant leading byte. `r` and `s` are always non-negative (RFC 3279's +//! `Ecdsa-Sig-Value` uses `INTEGER` for values that are never negative in practice), so +//! [`parse_integer`] rejects the "all ones" (negative) case unconditionally rather than checking +//! it for redundancy, and rejects a redundant leading `0x00` per §8.3.2's "all zero" case. +//! +//! [`decode`] additionally checks that parsing the `SEQUENCE`'s two `INTEGER`s consumes it exactly +//! (no gap, no overlap) and that the `SEQUENCE`'s own declared length accounts for every byte +//! passed in (no trailing garbage after it) -- both are ordinary structural well-formedness checks +//! for a length-prefixed format, not DER-specific, but easy to get wrong, so they're covered by +//! this module's own tests (`der_tests.rs`) as well as this crate's wycheproof suites, several of +//! which include DER-encoding-error test vectors. + +/// Upper bound on the DER encoding of a `SEQUENCE { r, s }` where `r` and `s` are each `n` bytes +/// wide (the curve's scalar width): each `INTEGER`'s content is at most `n + 1` bytes (the `+1` for +/// a sign-avoiding `0x00` pad, per this module's docs), so each `INTEGER` TLV is at most `n + 3` +/// bytes (tag + one length octet -- `n + 1 <= 67` for every curve this crate supports, always +/// under the 128 that would force a long-form length), the `SEQUENCE`'s content is at most +/// `2*(n + 3)`, and its own header is 2 bytes (short-form length) unless that content is `>= 128`, +/// in which case it needs 3 (long-form, one subsequent length octet -- true for P-521's `n = 66`, +/// where content is `138`). +pub const fn max_len(n: usize) -> usize { + let content_len = 2 * (n + 3); + let header_len = if content_len < 0x80 { 2 } else { 3 }; + header_len + content_len +} + +/// Writes `content_len`'s DER length octets (short or one-octet long form; see the module docs) to +/// `out[pos..]`, returning the number of octets written. +fn write_length(content_len: usize, out: &mut [u8], pos: usize) -> usize { + if content_len < 0x80 { + out[pos] = content_len as u8; + 1 + } else { + out[pos] = 0x81; + out[pos + 1] = content_len as u8; + 2 + } +} + +/// The DER content-octet length of `v`'s `INTEGER` encoding: `v` with its redundant leading zero +/// bytes stripped, plus one more if the remaining leading byte's high bit is set (to keep the +/// two's-complement value non-negative). +fn integer_content_len(v: &[u8]) -> usize { + let mut i = 0; + while i < v.len() - 1 && v[i] == 0 { + i += 1; + } + (v.len() - i) + if v[i] & 0x80 != 0 { 1 } else { 0 } +} + +/// Writes `v`'s DER `INTEGER` TLV to `out[pos..]`, returning the number of octets written. +fn write_integer(v: &[u8], out: &mut [u8], pos: usize) -> usize { + let mut i = 0; + while i < v.len() - 1 && v[i] == 0 { + i += 1; + } + let needs_pad = v[i] & 0x80 != 0; + let content_len = (v.len() - i) + if needs_pad { 1 } else { 0 }; + + out[pos] = 0x02; + let mut p = pos + 1 + write_length(content_len, out, pos + 1); + if needs_pad { + out[p] = 0; + p += 1; + } + out[p..p + (v.len() - i)].copy_from_slice(&v[i..]); + p + (v.len() - i) +} + +/// Encodes `r`/`s` (equal-length big-endian magnitudes) as a DER `SEQUENCE { r INTEGER, s INTEGER +/// }` into `out`, returning the number of bytes written. `out` must be at least +/// `max_len(r.len())` bytes and `r.len() == s.len()`; every call site in this crate sizes its own +/// buffer via [`max_len`] and passes equal-width `r`/`s` (both encode the same curve's scalars), so +/// this is an internal-crate contract, not a public API needing its own error path. +pub fn encode(r: &[u8], s: &[u8], out: &mut [u8]) -> usize { + debug_assert_eq!(r.len(), s.len(), "r and s must be the same width"); + let content_len = 2 + integer_content_len(r) + 2 + integer_content_len(s); + out[0] = 0x30; + let mut pos = 1 + write_length(content_len, out, 1); + pos = write_integer(r, out, pos); + write_integer(s, out, pos) +} + +/// Parses DER length octets at `bytes[pos..]` (short form, or the one-octet long form -- see the +/// module docs), returning `(length, octets_consumed)`, or `None` for the indefinite form, an +/// unsupported longer long-form, or a non-minimal long-form encoding (DER forbids both). +fn parse_length(bytes: &[u8], pos: usize) -> Option<(usize, usize)> { + let first = *bytes.get(pos)?; + if first < 0x80 { + Some((first as usize, 1)) + } else if first == 0x81 { + let len = *bytes.get(pos + 1)? as usize; + if len < 0x80 { + return None; // non-minimal: this length should have used the short form + } + Some((len, 2)) + } else { + None // indefinite (0x80) or a length-of-length this crate never needs (>= 0x82) + } +} + +/// Parses a DER `INTEGER` TLV at `bytes[pos..]`, writing its big-endian magnitude right-aligned +/// (zero-padded on the left) into `out`, and returns the number of bytes consumed -- or `None` if +/// malformed, negative, non-canonically padded (§8.3.2), or too wide for `out`. +fn parse_integer(bytes: &[u8], pos: usize, out: &mut [u8]) -> Option { + if *bytes.get(pos)? != 0x02 { + return None; + } + let (content_len, len_octets) = parse_length(bytes, pos + 1)?; + let content_start = pos + 1 + len_octets; + let content = bytes.get(content_start..content_start + content_len)?; + + let (first, rest) = content.split_first()?; // §8.3.1: content is never empty + if first & 0x80 != 0 { + return None; // negative: r, s are never negative (see the module docs) + } + if *first == 0 && rest.first().is_some_and(|b| b & 0x80 == 0) { + return None; // §8.3.2: redundant leading zero byte + } + // A leading 0x00 here (content.len() > 1, since a lone 0x00 is the encoding of zero) is only + // ever the sign-avoidance pad byte just validated above, not part of the magnitude, so it must + // not count against out's width. (The `content.len() > 1` guard is mutation-equivalent for + // this function's actual output: dropping it would make `magnitude` an empty slice for the + // `content == [0x00]` case, but `out` is already zero-filled below, so the written result is + // the same all-zero encoding of `0` either way.) + let magnitude = if *first == 0 && content.len() > 1 { rest } else { content }; + if magnitude.len() > out.len() { + return None; // magnitude too wide for this curve's scalar width + } + + out.fill(0); + let split = out.len() - magnitude.len(); + out[split..].copy_from_slice(magnitude); + Some(1 + len_octets + content_len) +} + +/// Decodes a DER `SEQUENCE { r, s }`, writing `r`/`s` right-aligned (zero-padded) into `r_out`/ +/// `s_out` (which must be the same length: the curve's scalar width), or `None` if malformed, if +/// either integer doesn't fit, or if `bytes` has trailing data beyond the `SEQUENCE` -- see the +/// module docs for exactly which checks this performs and why. +pub fn decode(bytes: &[u8], r_out: &mut [u8], s_out: &mut [u8]) -> Option<()> { + if r_out.len() != s_out.len() { + return None; + } + if *bytes.first()? != 0x30 { + return None; + } + let (seq_content_len, len_octets) = parse_length(bytes, 1)?; + let content_start = 1 + len_octets; + if content_start.checked_add(seq_content_len)? != bytes.len() { + return None; // trailing bytes after the SEQUENCE, or a declared length past the end + } + + let r_len = parse_integer(bytes, content_start, r_out)?; + let s_len = parse_integer(bytes, content_start + r_len, s_out)?; + if r_len + s_len != seq_content_len { + return None; // the two INTEGERs must exactly fill the declared SEQUENCE content + } + Some(()) +} diff --git a/crypto/ecdsa/src/ecdsa_bp256r1.rs b/crypto/ecdsa/src/ecdsa_bp256r1.rs new file mode 100644 index 000000000..e0d49a313 --- /dev/null +++ b/crypto/ecdsa/src/ecdsa_bp256r1.rs @@ -0,0 +1,422 @@ +//! ECDSA over brainpoolP256r1: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), +//! fixed to SHA-256. [`ECDSABp256r1`] is the algorithm marker; [`ECDSABp256r1PrivateKey`] signs +//! through [`SigningKey`] (random `k`, §6.3.1) and [`DeterministicSigningKey`] (`k` by RFC 6979, +//! §6.3.2), [`ECDSABp256r1PublicKey`] verifies through [`VerifyingKey`], as [`crate::ecdsa_p256`] +//! lays out. Fixed to SHA-256 (the +//! pairing wycheproof's `ecdsa_brainpoolP256r1_sha256_*` vector files use). Identical in shape to +//! [`crate::ecdsa_p256`] -- see that module's docs for the full reasoning (`e = bits2int(H)` needs +//! no truncation since `len(n) == hashlen == 256`; `x_affine_of_signing_point`'s +//! branch-free-on-a-secret-value argument; both hold unchanged here) -- with brainpoolP256r1's +//! types, [`crate::keys_bp256r1`], [`bouncycastle_ec::bp256r1_extra_bits`], and [`crate::rfc6979_bp256r1`] +//! substituted. + +use crate::keys_bp256r1::{ECDSABp256r1PrivateKey, ECDSABp256r1PublicKey}; +use crate::rfc6979_bp256r1; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::signature_key::{ + DeterministicSigningKey, SignatureAlgorithm, SigningKey, VerifyingKey, +}; +use bouncycastle_core::traits::{ + Algorithm, AlgorithmInfo, Hash, RNG, SignatureSigner, SignatureVerifier, +}; +use bouncycastle_ec::bp256r1::Bp256r1FieldElement; +use bouncycastle_ec::bp256r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp256r1_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::bp256r1_point::Bp256r1JacobianPoint; +use bouncycastle_ec::bp256r1_scalar::{ + Bp256r1PublicScalar, Bp256r1Scalar, Bp256r1ScalarField, N_LIMBS, +}; +use bouncycastle_ec::bp256r1_sec1; +use bouncycastle_ec::bp256r1_wnaf::shamir_multiply; +use bouncycastle_ec::nat; +use bouncycastle_sha2::SHA256; +use bouncycastle_utils::secret::Secret; + +/// Raw `r || s` signature length: two 32-byte field-width integers, the default encoding. +/// [`ECDSABp256r1PrivateKey::sign_der`]/[`ECDSABp256r1PublicKey::verify_der`] offer the RFC 3279 +/// §2.2.3 DER `SEQUENCE { r, s }` alternative via [`crate::der`], for interop that needs it. +pub const SIG_LEN: usize = 64; + +/// Upper bound on a DER-encoded brainpoolP256r1 signature (`r`, `s` each 32 bytes); see +/// [`crate::der::max_len`]. +pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(32); + +/// ECDSA over brainpoolP256r1: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), fixed +/// to SHA-256. The algorithm marker the key role traits are implemented over: +/// [`ECDSABp256r1PrivateKey`] signs with it through [`SigningKey`] (a random per-message `k`, +/// §6.3.1 / Appendix A.3.1) and [`DeterministicSigningKey`] (`k` from `d` and the message hash, +/// §6.3.2 / Appendix A.3.3, RFC 6979); [`ECDSABp256r1PublicKey`] verifies through [`VerifyingKey`]. +/// §6 says the choice of `k` "does not impact the signature verification process", so there is +/// one verifier and a signature from either signer verifies under it. +/// +/// Neither signer uses `ctx`: ECDSA has no context string, and the argument is accepted and +/// ignored so that the key traits read the same across algorithms. +pub struct ECDSABp256r1; + +impl Algorithm for ECDSABp256r1 { + const ALG_NAME: &'static str = "ECDSA-brainpoolP256r1"; + /// SP 800-57 Part 1 Rev. 5, Table 2: a 256-bit curve order gives 128 bits. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl SignatureAlgorithm for ECDSABp256r1 {} + +/// What [`ECDetDSABp256r1Signer`] reports as its algorithm name. The algorithm marker is still +/// [`ECDSABp256r1`]; this names the RFC 6979 way of choosing `k`. +pub const ECDETDSA_BP256R1_NAME: &str = "ECDetDSA-brainpoolP256r1"; + +/// The signer [`SigningKey::create_signer`] hands out: ECDSA with a random `k`. +/// +/// `k` is drawn from the caller's RNG when the signer is created (Appendix A.3.1, "extra random +/// bits"), since it does not depend on the message; the signer then only hashes. Two signatures +/// of one message under one key differ. +pub struct ECDSABp256r1Signer<'k> { + sk: &'k ECDSABp256r1PrivateKey, + hash: SHA256, + k: Bp256r1Scalar, +} + +/// The signer [`DeterministicSigningKey::create_signer_deterministic`] hands out: Deterministic +/// ECDSA, `k` from `d` and the message hash by RFC 6979, so the same key and message always give +/// the same signature and signing draws no randomness. Reports [`ECDETDSA_BP256R1_NAME`]. +pub struct ECDetDSABp256r1Signer<'k> { + sk: &'k ECDSABp256r1PrivateKey, + hash: SHA256, +} + +/// The verifier [`VerifyingKey::create_verifier`] hands out; FIPS 186-5 §6.4.2. +pub struct ECDSABp256r1Verifier<'k> { + pk: &'k ECDSABp256r1PublicKey, + hash: SHA256, +} + +/// `x * z^-2`, branch-free -- see [`crate::ecdsa_p256`]'s module docs for why this isn't +/// [`Bp256r1JacobianPoint::to_affine`]. +fn x_affine_of_signing_point(point: &Bp256r1JacobianPoint) -> Bp256r1FieldElement { + let z_inv = point.z.invert(); + point.x.mul(&z_inv.mul(&z_inv)) +} + +/// FIPS 186-5 §6.4.1 steps 4-12, given `k` already generated (step 3) by one of Appendix A.3's +/// methods. +/// Returns `None` for step 11's `r = 0` or `s = 0` case; each caller acts on it as step 11 +/// directs for its way of generating `k`. +fn sign_with_k( + sk: &ECDSABp256r1PrivateKey, + e: &Bp256r1ScalarField, + k: Bp256r1Scalar, +) -> Option<[u8; SIG_LEN]> { + let mut k_field = Bp256r1ScalarField::from_secret(&k); + let mut k_inv = k_field.invert(); // step 4 + + let r_point = comb_multiply_base_point(&k); // step 5 + let x_r = x_affine_of_signing_point(&r_point); // steps 6-7 + let r = Bp256r1ScalarField::from_limbs(x_r.to_limbs()); // step 8 + + let mut d_field = Bp256r1ScalarField::from_secret(sk.scalar()); + let s = k_inv.mul(&e.add(&r.mul(&d_field))); // step 9 + + // `k_field`, `k_inv` and `d_field` hold `k`, `k^-1` and `d`. Nothing below needs them, so they + // are overwritten here rather than left legible on the stack after this returns -- placed + // before step 11's check so that both ways out of the function are covered. `k` itself is a + // `Secret` and scrubs when it drops; `e`, `r` and `s` are public by construction. + k_field.zeroize(); + k_inv.zeroize(); + d_field.zeroize(); + + // step 11: astronomically unlikely (r, s range over ~2^256 values each); this comparison is + // FIPS 186-5's own mandated success/failure branch on the just-computed public output, not a + // constant-time violation -- see this module's docs on x_affine_of_signing_point for the one + // secret-derived branch this function does avoid. + if r_or_s_is_zero(&r, &s) { + return None; + } + + let mut sig = [0u8; SIG_LEN]; + sig[..32].copy_from_slice(&bp256r1_sec1::be_bytes_from_limbs(&r.to_limbs())); + sig[32..].copy_from_slice(&bp256r1_sec1::be_bytes_from_limbs(&s.to_limbs())); + Some(sig) +} + +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1, reduced into `[1, n-1]` by +/// `reduce_wide_bits_mod_n_minus_1`; the one place [`ECDSABp256r1`]'s signing paths draw `k`. +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; crate::keys_bp256r1::EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + +/// `e` from a 32-byte message hash: FIPS 186-5 §6.4.1 step 2 / §6.4.2 step 3, `E = H` (no +/// truncation needed -- see the module docs) then Appendix B.2.1's big-endian conversion. +fn e_from_hash(h: &[u8; 32]) -> Bp256r1ScalarField { + Bp256r1ScalarField::from_limbs(bp256r1_sec1::limbs_from_be_bytes(h)) +} + +/// FIPS 186-5 §6.4.1 step 11's `r = 0` or `s = 0` check, named so it has something to unit test +/// directly: reaching either branch through `sign_with_k` needs an `r`/`s` value that is +/// astronomically unlikely for any real `k`/`d`/message (see [`sign_with_k`]'s call site). +fn r_or_s_is_zero(r: &Bp256r1ScalarField, s: &Bp256r1ScalarField) -> bool { + *r == Bp256r1ScalarField::ZERO || *s == Bp256r1ScalarField::ZERO +} + +/// FIPS 186-5 §6.4.2 on the finished message hash; the verifier's `do_verify_final` is +/// this one function. See `crate::ecdsa_p256`'s `verify_final`. +fn verify_final( + hash: SHA256, + pk: &ECDSABp256r1PublicKey, + sig: &[u8], +) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::LengthError( + "ECDSA brainpoolP256r1 signature must be exactly 64 bytes", + )); + } + + // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. + let r_limbs = bp256r1_sec1::limbs_from_be_bytes(&sig[..32].try_into().unwrap()); + let s_limbs = bp256r1_sec1::limbs_from_be_bytes(&sig[32..64].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = Bp256r1ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 32]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); // steps 2-3 + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = Bp256r1PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 + let u = e.mul(&s_inv); // step 5 + let v = r.mul(&s_inv); + + let q = Bp256r1JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = Bp256r1PublicScalar::from_limbs(u.to_limbs()); + let v_pub = Bp256r1PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = Bp256r1ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; see +/// `crate::ecdsa_p256`'s `der_encode`. +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..32], &raw[32..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(32); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + +impl ECDSABp256r1PrivateKey { + /// [`SigningKey::sign`] with the signature DER-encoded as RFC 3279 §2.2.3's + /// `SEQUENCE { r, s }` (see [`crate::der`]); returns the buffer and the encoded length. + pub fn sign_der( + &self, + rng: &mut dyn RNG, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign(rng, msg, ctx)?)) + } + + /// [`DeterministicSigningKey::sign_deterministic`], DER-encoded as [`Self::sign_der`]. + pub fn sign_der_deterministic( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign_deterministic(msg, ctx)?)) + } +} + +impl ECDSABp256r1PublicKey { + /// [`VerifyingKey::verify`] of a DER-encoded signature (RFC 3279 §2.2.3); a malformed + /// encoding is a verification failure. + pub fn verify_der( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + self.verify(msg, ctx, &der_decode(sig)?) + } +} + +impl SigningKey for ECDSABp256r1PrivateKey { + type Signer<'k> + = ECDSABp256r1Signer<'k> + where + Self: 'k; + + fn create_signer<'k>( + &'k self, + rng: &mut dyn RNG, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSABp256r1Signer { sk: self, hash: SHA256::default(), k: random_k(rng)? }) + } +} + +impl DeterministicSigningKey for ECDSABp256r1PrivateKey { + type Signer<'k> + = ECDetDSABp256r1Signer<'k> + where + Self: 'k; + + fn create_signer_deterministic<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDetDSABp256r1Signer { sk: self, hash: SHA256::default() }) + } +} + +impl VerifyingKey for ECDSABp256r1PublicKey { + type Verifier<'k> + = ECDSABp256r1Verifier<'k> + where + Self: 'k; + + fn create_verifier<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSABp256r1Verifier { pk: self, hash: SHA256::default() }) + } +} + +impl AlgorithmInfo for ECDSABp256r1Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDSABp256r1::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSABp256r1::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDSABp256r1Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 32]; + self.hash.do_final_out(&mut h); + // FIPS 186-5 §6.4.1 step 11: with a random `k`, `r = 0` or `s = 0` goes back to step 3. + // `k` was drawn when this signer was created and there is no RNG here to draw another + // from, so the failure is reported instead; it has probability about 2^-255. + sign_with_k(self.sk, &e_from_hash(&h), self.k).ok_or(SignatureError::GenericError( + "ECDSA produced r = 0 or s = 0; create a fresh signer and sign again", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDetDSABp256r1Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDETDSA_BP256R1_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSABp256r1::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDetDSABp256r1Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 32]; + self.hash.do_final_out(&mut h); + let k = rfc6979_bp256r1::generate_k(self.sk.scalar(), &h); // step 3, Appendix A.3.3 + // FIPS 186-5 §6.4.1 step 11: with a deterministic `k`, `r = 0` or `s = 0` outputs failure. + sign_with_k(self.sk, &e_from_hash(&h), k).ok_or(SignatureError::GenericError( + "deterministic ECDSA produced r = 0 or s = 0; FIPS 186-5 §6.4.1 step 11: output failure", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDSABp256r1Verifier<'_> { + fn alg_name(&self) -> &'static str { + ECDSABp256r1::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSABp256r1::MAX_SECURITY_STRENGTH + } +} + +impl SignatureVerifier for ECDSABp256r1Verifier<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + verify_final(self.hash, self.pk, sig) + } +} + +fn in_range(limbs: &[u64; 4]) -> bool { + if *limbs == [0, 0, 0, 0] { + return false; + } + let (_, borrow) = nat::sub(limbs, &N_LIMBS); + borrow == 1 +} + +// `r_or_s_is_zero` is private and, through `sign_with_k`, only ever reached with an `r`/`s` value +// that is astronomically unlikely for any real input (see its own doc comment), so no realistic +// integration test exercises either disjunct -- the QUALITY_AND_STYLE.md private-function +// exception applies. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn r_or_s_is_zero_requires_only_one_to_be_zero() { + let zero = Bp256r1ScalarField::ZERO; + let one = Bp256r1ScalarField::ONE; + assert!(!r_or_s_is_zero(&one, &one)); + assert!(r_or_s_is_zero(&zero, &one)); + assert!(r_or_s_is_zero(&one, &zero)); + assert!(r_or_s_is_zero(&zero, &zero)); + } +} diff --git a/crypto/ecdsa/src/ecdsa_bp384r1.rs b/crypto/ecdsa/src/ecdsa_bp384r1.rs new file mode 100644 index 000000000..872cad49f --- /dev/null +++ b/crypto/ecdsa/src/ecdsa_bp384r1.rs @@ -0,0 +1,422 @@ +//! ECDSA over brainpoolP384r1: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), +//! fixed to SHA-384. [`ECDSABp384r1`] is the algorithm marker; [`ECDSABp384r1PrivateKey`] signs +//! through [`SigningKey`] (random `k`, §6.3.1) and [`DeterministicSigningKey`] (`k` by RFC 6979, +//! §6.3.2), [`ECDSABp384r1PublicKey`] verifies through [`VerifyingKey`], as [`crate::ecdsa_p256`] +//! lays out. Fixed to SHA-384 (the +//! pairing wycheproof's `ecdsa_brainpoolP384r1_sha384_*` vector files use). Identical in shape to +//! [`crate::ecdsa_p256`] -- see that module's docs for the full reasoning (`e = bits2int(H)` needs +//! no truncation since `len(n) == hashlen == 384`; `x_affine_of_signing_point`'s +//! branch-free-on-a-secret-value argument; both hold unchanged here) -- with brainpoolP384r1's +//! types, [`crate::keys_bp384r1`], [`bouncycastle_ec::bp384r1_extra_bits`], and [`crate::rfc6979_bp384r1`] +//! substituted. + +use crate::keys_bp384r1::{ECDSABp384r1PrivateKey, ECDSABp384r1PublicKey}; +use crate::rfc6979_bp384r1; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::signature_key::{ + DeterministicSigningKey, SignatureAlgorithm, SigningKey, VerifyingKey, +}; +use bouncycastle_core::traits::{ + Algorithm, AlgorithmInfo, Hash, RNG, SignatureSigner, SignatureVerifier, +}; +use bouncycastle_ec::bp384r1::Bp384r1FieldElement; +use bouncycastle_ec::bp384r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp384r1_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::bp384r1_point::Bp384r1JacobianPoint; +use bouncycastle_ec::bp384r1_scalar::{ + Bp384r1PublicScalar, Bp384r1Scalar, Bp384r1ScalarField, N_LIMBS, +}; +use bouncycastle_ec::bp384r1_sec1; +use bouncycastle_ec::bp384r1_wnaf::shamir_multiply; +use bouncycastle_ec::nat; +use bouncycastle_sha2::SHA384; +use bouncycastle_utils::secret::Secret; + +/// Raw `r || s` signature length: two 48-byte field-width integers, the default encoding. +/// [`ECDSABp384r1PrivateKey::sign_der`]/[`ECDSABp384r1PublicKey::verify_der`] offer the RFC 3279 +/// §2.2.3 DER `SEQUENCE { r, s }` alternative via [`crate::der`], for interop that needs it. +pub const SIG_LEN: usize = 96; + +/// Upper bound on a DER-encoded brainpoolP384r1 signature (`r`, `s` each 48 bytes); see +/// [`crate::der::max_len`]. +pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(48); + +/// ECDSA over brainpoolP384r1: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), fixed +/// to SHA-384. The algorithm marker the key role traits are implemented over: +/// [`ECDSABp384r1PrivateKey`] signs with it through [`SigningKey`] (a random per-message `k`, +/// §6.3.1 / Appendix A.3.1) and [`DeterministicSigningKey`] (`k` from `d` and the message hash, +/// §6.3.2 / Appendix A.3.3, RFC 6979); [`ECDSABp384r1PublicKey`] verifies through [`VerifyingKey`]. +/// §6 says the choice of `k` "does not impact the signature verification process", so there is +/// one verifier and a signature from either signer verifies under it. +/// +/// Neither signer uses `ctx`: ECDSA has no context string, and the argument is accepted and +/// ignored so that the key traits read the same across algorithms. +pub struct ECDSABp384r1; + +impl Algorithm for ECDSABp384r1 { + const ALG_NAME: &'static str = "ECDSA-brainpoolP384r1"; + /// SP 800-57 Part 1 Rev. 5, Table 2: a 384-bit curve order gives 192 bits. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; +} + +impl SignatureAlgorithm for ECDSABp384r1 {} + +/// What [`ECDetDSABp384r1Signer`] reports as its algorithm name. The algorithm marker is still +/// [`ECDSABp384r1`]; this names the RFC 6979 way of choosing `k`. +pub const ECDETDSA_BP384R1_NAME: &str = "ECDetDSA-brainpoolP384r1"; + +/// The signer [`SigningKey::create_signer`] hands out: ECDSA with a random `k`. +/// +/// `k` is drawn from the caller's RNG when the signer is created (Appendix A.3.1, "extra random +/// bits"), since it does not depend on the message; the signer then only hashes. Two signatures +/// of one message under one key differ. +pub struct ECDSABp384r1Signer<'k> { + sk: &'k ECDSABp384r1PrivateKey, + hash: SHA384, + k: Bp384r1Scalar, +} + +/// The signer [`DeterministicSigningKey::create_signer_deterministic`] hands out: Deterministic +/// ECDSA, `k` from `d` and the message hash by RFC 6979, so the same key and message always give +/// the same signature and signing draws no randomness. Reports [`ECDETDSA_BP384R1_NAME`]. +pub struct ECDetDSABp384r1Signer<'k> { + sk: &'k ECDSABp384r1PrivateKey, + hash: SHA384, +} + +/// The verifier [`VerifyingKey::create_verifier`] hands out; FIPS 186-5 §6.4.2. +pub struct ECDSABp384r1Verifier<'k> { + pk: &'k ECDSABp384r1PublicKey, + hash: SHA384, +} + +/// `x * z^-2`, branch-free -- see [`crate::ecdsa_p256`]'s module docs for why this isn't +/// [`Bp384r1JacobianPoint::to_affine`]. +fn x_affine_of_signing_point(point: &Bp384r1JacobianPoint) -> Bp384r1FieldElement { + let z_inv = point.z.invert(); + point.x.mul(&z_inv.mul(&z_inv)) +} + +/// FIPS 186-5 §6.4.1 steps 4-12, given `k` already generated (step 3) by one of Appendix A.3's +/// methods. +/// Returns `None` for step 11's `r = 0` or `s = 0` case; each caller acts on it as step 11 +/// directs for its way of generating `k`. +fn sign_with_k( + sk: &ECDSABp384r1PrivateKey, + e: &Bp384r1ScalarField, + k: Bp384r1Scalar, +) -> Option<[u8; SIG_LEN]> { + let mut k_field = Bp384r1ScalarField::from_secret(&k); + let mut k_inv = k_field.invert(); // step 4 + + let r_point = comb_multiply_base_point(&k); // step 5 + let x_r = x_affine_of_signing_point(&r_point); // steps 6-7 + let r = Bp384r1ScalarField::from_limbs(x_r.to_limbs()); // step 8 + + let mut d_field = Bp384r1ScalarField::from_secret(sk.scalar()); + let s = k_inv.mul(&e.add(&r.mul(&d_field))); // step 9 + + // `k_field`, `k_inv` and `d_field` hold `k`, `k^-1` and `d`. Nothing below needs them, so they + // are overwritten here rather than left legible on the stack after this returns -- placed + // before step 11's check so that both ways out of the function are covered. `k` itself is a + // `Secret` and scrubs when it drops; `e`, `r` and `s` are public by construction. + k_field.zeroize(); + k_inv.zeroize(); + d_field.zeroize(); + + // step 11: astronomically unlikely (r, s range over ~2^384 values each); this comparison is + // FIPS 186-5's own mandated success/failure branch on the just-computed public output, not a + // constant-time violation -- see this module's docs on x_affine_of_signing_point for the one + // secret-derived branch this function does avoid. + if r_or_s_is_zero(&r, &s) { + return None; + } + + let mut sig = [0u8; SIG_LEN]; + sig[..48].copy_from_slice(&bp384r1_sec1::be_bytes_from_limbs(&r.to_limbs())); + sig[48..].copy_from_slice(&bp384r1_sec1::be_bytes_from_limbs(&s.to_limbs())); + Some(sig) +} + +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1, reduced into `[1, n-1]` by +/// `reduce_wide_bits_mod_n_minus_1`; the one place [`ECDSABp384r1`]'s signing paths draw `k`. +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; crate::keys_bp384r1::EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + +/// `e` from a 48-byte message hash: FIPS 186-5 §6.4.1 step 2 / §6.4.2 step 3, `E = H` (no +/// truncation needed -- see the module docs) then Appendix B.2.1's big-endian conversion. +fn e_from_hash(h: &[u8; 48]) -> Bp384r1ScalarField { + Bp384r1ScalarField::from_limbs(bp384r1_sec1::limbs_from_be_bytes(h)) +} + +/// FIPS 186-5 §6.4.1 step 11's `r = 0` or `s = 0` check, named so it has something to unit test +/// directly: reaching either branch through `sign_with_k` needs an `r`/`s` value that is +/// astronomically unlikely for any real `k`/`d`/message (see [`sign_with_k`]'s call site). +fn r_or_s_is_zero(r: &Bp384r1ScalarField, s: &Bp384r1ScalarField) -> bool { + *r == Bp384r1ScalarField::ZERO || *s == Bp384r1ScalarField::ZERO +} + +/// FIPS 186-5 §6.4.2 on the finished message hash; the verifier's `do_verify_final` is +/// this one function. See `crate::ecdsa_p256`'s `verify_final`. +fn verify_final( + hash: SHA384, + pk: &ECDSABp384r1PublicKey, + sig: &[u8], +) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::LengthError( + "ECDSA brainpoolP384r1 signature must be exactly 96 bytes", + )); + } + + // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. + let r_limbs = bp384r1_sec1::limbs_from_be_bytes(&sig[..48].try_into().unwrap()); + let s_limbs = bp384r1_sec1::limbs_from_be_bytes(&sig[48..96].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = Bp384r1ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 48]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); // steps 2-3 + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = Bp384r1PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 + let u = e.mul(&s_inv); // step 5 + let v = r.mul(&s_inv); + + let q = Bp384r1JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = Bp384r1PublicScalar::from_limbs(u.to_limbs()); + let v_pub = Bp384r1PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = Bp384r1ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; see +/// `crate::ecdsa_p256`'s `der_encode`. +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..48], &raw[48..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(48); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + +impl ECDSABp384r1PrivateKey { + /// [`SigningKey::sign`] with the signature DER-encoded as RFC 3279 §2.2.3's + /// `SEQUENCE { r, s }` (see [`crate::der`]); returns the buffer and the encoded length. + pub fn sign_der( + &self, + rng: &mut dyn RNG, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign(rng, msg, ctx)?)) + } + + /// [`DeterministicSigningKey::sign_deterministic`], DER-encoded as [`Self::sign_der`]. + pub fn sign_der_deterministic( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign_deterministic(msg, ctx)?)) + } +} + +impl ECDSABp384r1PublicKey { + /// [`VerifyingKey::verify`] of a DER-encoded signature (RFC 3279 §2.2.3); a malformed + /// encoding is a verification failure. + pub fn verify_der( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + self.verify(msg, ctx, &der_decode(sig)?) + } +} + +impl SigningKey for ECDSABp384r1PrivateKey { + type Signer<'k> + = ECDSABp384r1Signer<'k> + where + Self: 'k; + + fn create_signer<'k>( + &'k self, + rng: &mut dyn RNG, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSABp384r1Signer { sk: self, hash: SHA384::default(), k: random_k(rng)? }) + } +} + +impl DeterministicSigningKey for ECDSABp384r1PrivateKey { + type Signer<'k> + = ECDetDSABp384r1Signer<'k> + where + Self: 'k; + + fn create_signer_deterministic<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDetDSABp384r1Signer { sk: self, hash: SHA384::default() }) + } +} + +impl VerifyingKey for ECDSABp384r1PublicKey { + type Verifier<'k> + = ECDSABp384r1Verifier<'k> + where + Self: 'k; + + fn create_verifier<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSABp384r1Verifier { pk: self, hash: SHA384::default() }) + } +} + +impl AlgorithmInfo for ECDSABp384r1Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDSABp384r1::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSABp384r1::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDSABp384r1Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 48]; + self.hash.do_final_out(&mut h); + // FIPS 186-5 §6.4.1 step 11: with a random `k`, `r = 0` or `s = 0` goes back to step 3. + // `k` was drawn when this signer was created and there is no RNG here to draw another + // from, so the failure is reported instead; it has probability about 2^-383. + sign_with_k(self.sk, &e_from_hash(&h), self.k).ok_or(SignatureError::GenericError( + "ECDSA produced r = 0 or s = 0; create a fresh signer and sign again", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDetDSABp384r1Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDETDSA_BP384R1_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSABp384r1::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDetDSABp384r1Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 48]; + self.hash.do_final_out(&mut h); + let k = rfc6979_bp384r1::generate_k(self.sk.scalar(), &h); // step 3, Appendix A.3.3 + // FIPS 186-5 §6.4.1 step 11: with a deterministic `k`, `r = 0` or `s = 0` outputs failure. + sign_with_k(self.sk, &e_from_hash(&h), k).ok_or(SignatureError::GenericError( + "deterministic ECDSA produced r = 0 or s = 0; FIPS 186-5 §6.4.1 step 11: output failure", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDSABp384r1Verifier<'_> { + fn alg_name(&self) -> &'static str { + ECDSABp384r1::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSABp384r1::MAX_SECURITY_STRENGTH + } +} + +impl SignatureVerifier for ECDSABp384r1Verifier<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + verify_final(self.hash, self.pk, sig) + } +} + +fn in_range(limbs: &[u64; 6]) -> bool { + if *limbs == [0, 0, 0, 0, 0, 0] { + return false; + } + let (_, borrow) = nat::sub(limbs, &N_LIMBS); + borrow == 1 +} + +// `r_or_s_is_zero` is private and, through `sign_with_k`, only ever reached with an `r`/`s` value +// that is astronomically unlikely for any real input (see its own doc comment), so no realistic +// integration test exercises either disjunct -- the QUALITY_AND_STYLE.md private-function +// exception applies. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn r_or_s_is_zero_requires_only_one_to_be_zero() { + let zero = Bp384r1ScalarField::ZERO; + let one = Bp384r1ScalarField::ONE; + assert!(!r_or_s_is_zero(&one, &one)); + assert!(r_or_s_is_zero(&zero, &one)); + assert!(r_or_s_is_zero(&one, &zero)); + assert!(r_or_s_is_zero(&zero, &zero)); + } +} diff --git a/crypto/ecdsa/src/ecdsa_bp512r1.rs b/crypto/ecdsa/src/ecdsa_bp512r1.rs new file mode 100644 index 000000000..b82951d89 --- /dev/null +++ b/crypto/ecdsa/src/ecdsa_bp512r1.rs @@ -0,0 +1,422 @@ +//! ECDSA over brainpoolP512r1: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), +//! fixed to SHA-512. [`ECDSABp512r1`] is the algorithm marker; [`ECDSABp512r1PrivateKey`] signs +//! through [`SigningKey`] (random `k`, §6.3.1) and [`DeterministicSigningKey`] (`k` by RFC 6979, +//! §6.3.2), [`ECDSABp512r1PublicKey`] verifies through [`VerifyingKey`], as [`crate::ecdsa_p256`] +//! lays out. Fixed to SHA-512 (the +//! pairing wycheproof's `ecdsa_brainpoolP512r1_sha512_*` vector files use). Identical in shape to +//! [`crate::ecdsa_p256`] -- see that module's docs for the full reasoning (`e = bits2int(H)` needs +//! no truncation since `len(n) == hashlen == 512`; `x_affine_of_signing_point`'s +//! branch-free-on-a-secret-value argument; both hold unchanged here) -- with brainpoolP512r1's +//! types, [`crate::keys_bp512r1`], [`bouncycastle_ec::bp512r1_extra_bits`], and [`crate::rfc6979_bp512r1`] +//! substituted. + +use crate::keys_bp512r1::{ECDSABp512r1PrivateKey, ECDSABp512r1PublicKey}; +use crate::rfc6979_bp512r1; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::signature_key::{ + DeterministicSigningKey, SignatureAlgorithm, SigningKey, VerifyingKey, +}; +use bouncycastle_core::traits::{ + Algorithm, AlgorithmInfo, Hash, RNG, SignatureSigner, SignatureVerifier, +}; +use bouncycastle_ec::bp512r1::Bp512r1FieldElement; +use bouncycastle_ec::bp512r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp512r1_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::bp512r1_point::Bp512r1JacobianPoint; +use bouncycastle_ec::bp512r1_scalar::{ + Bp512r1PublicScalar, Bp512r1Scalar, Bp512r1ScalarField, N_LIMBS, +}; +use bouncycastle_ec::bp512r1_sec1; +use bouncycastle_ec::bp512r1_wnaf::shamir_multiply; +use bouncycastle_ec::nat; +use bouncycastle_sha2::SHA512; +use bouncycastle_utils::secret::Secret; + +/// Raw `r || s` signature length: two 64-byte field-width integers, the default encoding. +/// [`ECDSABp512r1PrivateKey::sign_der`]/[`ECDSABp512r1PublicKey::verify_der`] offer the RFC 3279 +/// §2.2.3 DER `SEQUENCE { r, s }` alternative via [`crate::der`], for interop that needs it. +pub const SIG_LEN: usize = 128; + +/// Upper bound on a DER-encoded brainpoolP512r1 signature (`r`, `s` each 64 bytes); see +/// [`crate::der::max_len`]. +pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(64); + +/// ECDSA over brainpoolP512r1: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), fixed +/// to SHA-512. The algorithm marker the key role traits are implemented over: +/// [`ECDSABp512r1PrivateKey`] signs with it through [`SigningKey`] (a random per-message `k`, +/// §6.3.1 / Appendix A.3.1) and [`DeterministicSigningKey`] (`k` from `d` and the message hash, +/// §6.3.2 / Appendix A.3.3, RFC 6979); [`ECDSABp512r1PublicKey`] verifies through [`VerifyingKey`]. +/// §6 says the choice of `k` "does not impact the signature verification process", so there is +/// one verifier and a signature from either signer verifies under it. +/// +/// Neither signer uses `ctx`: ECDSA has no context string, and the argument is accepted and +/// ignored so that the key traits read the same across algorithms. +pub struct ECDSABp512r1; + +impl Algorithm for ECDSABp512r1 { + const ALG_NAME: &'static str = "ECDSA-brainpoolP512r1"; + /// SP 800-57 Part 1 Rev. 5, Table 2: a 512-bit curve order gives 256 bits. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +} + +impl SignatureAlgorithm for ECDSABp512r1 {} + +/// What [`ECDetDSABp512r1Signer`] reports as its algorithm name. The algorithm marker is still +/// [`ECDSABp512r1`]; this names the RFC 6979 way of choosing `k`. +pub const ECDETDSA_BP512R1_NAME: &str = "ECDetDSA-brainpoolP512r1"; + +/// The signer [`SigningKey::create_signer`] hands out: ECDSA with a random `k`. +/// +/// `k` is drawn from the caller's RNG when the signer is created (Appendix A.3.1, "extra random +/// bits"), since it does not depend on the message; the signer then only hashes. Two signatures +/// of one message under one key differ. +pub struct ECDSABp512r1Signer<'k> { + sk: &'k ECDSABp512r1PrivateKey, + hash: SHA512, + k: Bp512r1Scalar, +} + +/// The signer [`DeterministicSigningKey::create_signer_deterministic`] hands out: Deterministic +/// ECDSA, `k` from `d` and the message hash by RFC 6979, so the same key and message always give +/// the same signature and signing draws no randomness. Reports [`ECDETDSA_BP512R1_NAME`]. +pub struct ECDetDSABp512r1Signer<'k> { + sk: &'k ECDSABp512r1PrivateKey, + hash: SHA512, +} + +/// The verifier [`VerifyingKey::create_verifier`] hands out; FIPS 186-5 §6.4.2. +pub struct ECDSABp512r1Verifier<'k> { + pk: &'k ECDSABp512r1PublicKey, + hash: SHA512, +} + +/// `x * z^-2`, branch-free -- see [`crate::ecdsa_p256`]'s module docs for why this isn't +/// [`Bp512r1JacobianPoint::to_affine`]. +fn x_affine_of_signing_point(point: &Bp512r1JacobianPoint) -> Bp512r1FieldElement { + let z_inv = point.z.invert(); + point.x.mul(&z_inv.mul(&z_inv)) +} + +/// FIPS 186-5 §6.4.1 steps 4-12, given `k` already generated (step 3) by one of Appendix A.3's +/// methods. +/// Returns `None` for step 11's `r = 0` or `s = 0` case; each caller acts on it as step 11 +/// directs for its way of generating `k`. +fn sign_with_k( + sk: &ECDSABp512r1PrivateKey, + e: &Bp512r1ScalarField, + k: Bp512r1Scalar, +) -> Option<[u8; SIG_LEN]> { + let mut k_field = Bp512r1ScalarField::from_secret(&k); + let mut k_inv = k_field.invert(); // step 4 + + let r_point = comb_multiply_base_point(&k); // step 5 + let x_r = x_affine_of_signing_point(&r_point); // steps 6-7 + let r = Bp512r1ScalarField::from_limbs(x_r.to_limbs()); // step 8 + + let mut d_field = Bp512r1ScalarField::from_secret(sk.scalar()); + let s = k_inv.mul(&e.add(&r.mul(&d_field))); // step 9 + + // `k_field`, `k_inv` and `d_field` hold `k`, `k^-1` and `d`. Nothing below needs them, so they + // are overwritten here rather than left legible on the stack after this returns -- placed + // before step 11's check so that both ways out of the function are covered. `k` itself is a + // `Secret` and scrubs when it drops; `e`, `r` and `s` are public by construction. + k_field.zeroize(); + k_inv.zeroize(); + d_field.zeroize(); + + // step 11: astronomically unlikely (r, s range over ~2^512 values each); this comparison is + // FIPS 186-5's own mandated success/failure branch on the just-computed public output, not a + // constant-time violation -- see this module's docs on x_affine_of_signing_point for the one + // secret-derived branch this function does avoid. + if r_or_s_is_zero(&r, &s) { + return None; + } + + let mut sig = [0u8; SIG_LEN]; + sig[..64].copy_from_slice(&bp512r1_sec1::be_bytes_from_limbs(&r.to_limbs())); + sig[64..].copy_from_slice(&bp512r1_sec1::be_bytes_from_limbs(&s.to_limbs())); + Some(sig) +} + +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1, reduced into `[1, n-1]` by +/// `reduce_wide_bits_mod_n_minus_1`; the one place [`ECDSABp512r1`]'s signing paths draw `k`. +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; crate::keys_bp512r1::EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + +/// `e` from a 64-byte message hash: FIPS 186-5 §6.4.1 step 2 / §6.4.2 step 3, `E = H` (no +/// truncation needed -- see the module docs) then Appendix B.2.1's big-endian conversion. +fn e_from_hash(h: &[u8; 64]) -> Bp512r1ScalarField { + Bp512r1ScalarField::from_limbs(bp512r1_sec1::limbs_from_be_bytes(h)) +} + +/// FIPS 186-5 §6.4.1 step 11's `r = 0` or `s = 0` check, named so it has something to unit test +/// directly: reaching either branch through `sign_with_k` needs an `r`/`s` value that is +/// astronomically unlikely for any real `k`/`d`/message (see [`sign_with_k`]'s call site). +fn r_or_s_is_zero(r: &Bp512r1ScalarField, s: &Bp512r1ScalarField) -> bool { + *r == Bp512r1ScalarField::ZERO || *s == Bp512r1ScalarField::ZERO +} + +/// FIPS 186-5 §6.4.2 on the finished message hash; the verifier's `do_verify_final` is +/// this one function. See `crate::ecdsa_p256`'s `verify_final`. +fn verify_final( + hash: SHA512, + pk: &ECDSABp512r1PublicKey, + sig: &[u8], +) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::LengthError( + "ECDSA brainpoolP512r1 signature must be exactly 128 bytes", + )); + } + + // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. + let r_limbs = bp512r1_sec1::limbs_from_be_bytes(&sig[..64].try_into().unwrap()); + let s_limbs = bp512r1_sec1::limbs_from_be_bytes(&sig[64..128].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = Bp512r1ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 64]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); // steps 2-3 + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = Bp512r1PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 + let u = e.mul(&s_inv); // step 5 + let v = r.mul(&s_inv); + + let q = Bp512r1JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = Bp512r1PublicScalar::from_limbs(u.to_limbs()); + let v_pub = Bp512r1PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = Bp512r1ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; see +/// `crate::ecdsa_p256`'s `der_encode`. +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..64], &raw[64..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(64); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + +impl ECDSABp512r1PrivateKey { + /// [`SigningKey::sign`] with the signature DER-encoded as RFC 3279 §2.2.3's + /// `SEQUENCE { r, s }` (see [`crate::der`]); returns the buffer and the encoded length. + pub fn sign_der( + &self, + rng: &mut dyn RNG, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign(rng, msg, ctx)?)) + } + + /// [`DeterministicSigningKey::sign_deterministic`], DER-encoded as [`Self::sign_der`]. + pub fn sign_der_deterministic( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign_deterministic(msg, ctx)?)) + } +} + +impl ECDSABp512r1PublicKey { + /// [`VerifyingKey::verify`] of a DER-encoded signature (RFC 3279 §2.2.3); a malformed + /// encoding is a verification failure. + pub fn verify_der( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + self.verify(msg, ctx, &der_decode(sig)?) + } +} + +impl SigningKey for ECDSABp512r1PrivateKey { + type Signer<'k> + = ECDSABp512r1Signer<'k> + where + Self: 'k; + + fn create_signer<'k>( + &'k self, + rng: &mut dyn RNG, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSABp512r1Signer { sk: self, hash: SHA512::default(), k: random_k(rng)? }) + } +} + +impl DeterministicSigningKey for ECDSABp512r1PrivateKey { + type Signer<'k> + = ECDetDSABp512r1Signer<'k> + where + Self: 'k; + + fn create_signer_deterministic<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDetDSABp512r1Signer { sk: self, hash: SHA512::default() }) + } +} + +impl VerifyingKey for ECDSABp512r1PublicKey { + type Verifier<'k> + = ECDSABp512r1Verifier<'k> + where + Self: 'k; + + fn create_verifier<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSABp512r1Verifier { pk: self, hash: SHA512::default() }) + } +} + +impl AlgorithmInfo for ECDSABp512r1Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDSABp512r1::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSABp512r1::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDSABp512r1Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 64]; + self.hash.do_final_out(&mut h); + // FIPS 186-5 §6.4.1 step 11: with a random `k`, `r = 0` or `s = 0` goes back to step 3. + // `k` was drawn when this signer was created and there is no RNG here to draw another + // from, so the failure is reported instead; it has probability about 2^-511. + sign_with_k(self.sk, &e_from_hash(&h), self.k).ok_or(SignatureError::GenericError( + "ECDSA produced r = 0 or s = 0; create a fresh signer and sign again", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDetDSABp512r1Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDETDSA_BP512R1_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSABp512r1::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDetDSABp512r1Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 64]; + self.hash.do_final_out(&mut h); + let k = rfc6979_bp512r1::generate_k(self.sk.scalar(), &h); // step 3, Appendix A.3.3 + // FIPS 186-5 §6.4.1 step 11: with a deterministic `k`, `r = 0` or `s = 0` outputs failure. + sign_with_k(self.sk, &e_from_hash(&h), k).ok_or(SignatureError::GenericError( + "deterministic ECDSA produced r = 0 or s = 0; FIPS 186-5 §6.4.1 step 11: output failure", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDSABp512r1Verifier<'_> { + fn alg_name(&self) -> &'static str { + ECDSABp512r1::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSABp512r1::MAX_SECURITY_STRENGTH + } +} + +impl SignatureVerifier for ECDSABp512r1Verifier<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + verify_final(self.hash, self.pk, sig) + } +} + +fn in_range(limbs: &[u64; 8]) -> bool { + if *limbs == [0, 0, 0, 0, 0, 0, 0, 0] { + return false; + } + let (_, borrow) = nat::sub(limbs, &N_LIMBS); + borrow == 1 +} + +// `r_or_s_is_zero` is private and, through `sign_with_k`, only ever reached with an `r`/`s` value +// that is astronomically unlikely for any real input (see its own doc comment), so no realistic +// integration test exercises either disjunct -- the QUALITY_AND_STYLE.md private-function +// exception applies. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn r_or_s_is_zero_requires_only_one_to_be_zero() { + let zero = Bp512r1ScalarField::ZERO; + let one = Bp512r1ScalarField::ONE; + assert!(!r_or_s_is_zero(&one, &one)); + assert!(r_or_s_is_zero(&zero, &one)); + assert!(r_or_s_is_zero(&one, &zero)); + assert!(r_or_s_is_zero(&zero, &zero)); + } +} diff --git a/crypto/ecdsa/src/ecdsa_p256.rs b/crypto/ecdsa/src/ecdsa_p256.rs new file mode 100644 index 000000000..837e71272 --- /dev/null +++ b/crypto/ecdsa/src/ecdsa_p256.rs @@ -0,0 +1,431 @@ +//! ECDSA over P-256: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), fixed +//! to SHA-256. [`ECDSAP256`] is the algorithm marker; [`ECDSAP256PrivateKey`] signs through +//! [`SigningKey`] (a random per-message `k`, §6.3.1 / Appendix A.3.1) and +//! [`DeterministicSigningKey`] (`k` from `d` and the message hash, §6.3.2 / Appendix A.3.3, +//! RFC 6979), and [`ECDSAP256PublicKey`] verifies through [`VerifyingKey`]. §6 says the choice of +//! `k` "does not impact the signature verification process", so there is one verifier and a +//! signature from either signer verifies under it. +//! +//! # `r`, `s` and `e` +//! +//! SP 800-186 Appendix F.1 (field element -> integer) is the identity for a prime field, so +//! extracting `r`'s raw value from `R`'s affine `x`-coordinate is just reading that coordinate's +//! canonical `[0, p)` representative. `e` (§6.4.1 step 2 / §6.4.2 step 3) needs no truncation +//! because `len(n) == hashlen == 256` for P-256/SHA-256, so `E = H` and `e = bits2int(H)` directly +//! (Appendix B.2.1). Both `r1` (from `x_R`) and `e` can exceed `n` as raw 256-bit values; +//! [`P256ScalarField::from_limbs`] reduces any such value with a single conditional subtraction +//! (documented on that function), which is exactly steps 6-8's "convert, then `mod n`". +//! +//! # Extracting `x_R` from a secret-derived `R` +//! +//! `R = [k]G` is Jacobian-coordinate output of a secret scalar multiplication, so converting it to +//! affine cannot use [`P256JacobianPoint::to_affine`] -- that function's own docs say it branches +//! on infinity and is "not for use on a secret intermediate value". [`x_affine_of_signing_point`] +//! instead always computes `X * Z^-2`, branch-free, exactly mirroring what `to_affine` does in its +//! non-infinity case; `Z` is never `0` here since `k` is in `[1, n-1]` (RFC 6979 or Appendix A.3.1, +//! both of which produce a `k` in that range by construction) and `G` has prime order `n` (SP +//! 800-186 §3.2.1.3's cofactor `h = 1`), so `[k]G` is never the identity for any valid `k` -- this +//! holds unconditionally, so skipping the infinity branch leaks nothing about which `k` was drawn. + +use crate::keys_p256::{ECDSAP256PrivateKey, ECDSAP256PublicKey, EXTRA_BITS_DRBG_OUTPUT_LEN}; +use crate::rfc6979_p256; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::signature_key::{ + DeterministicSigningKey, SignatureAlgorithm, SigningKey, VerifyingKey, +}; +use bouncycastle_core::traits::{ + Algorithm, AlgorithmInfo, Hash, RNG, SignatureSigner, SignatureVerifier, +}; +use bouncycastle_ec::nat; +use bouncycastle_ec::p256::P256FieldElement; +use bouncycastle_ec::p256_comb::comb_multiply_base_point; +use bouncycastle_ec::p256_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::p256_point::P256JacobianPoint; +use bouncycastle_ec::p256_scalar::{N_LIMBS, P256PublicScalar, P256Scalar, P256ScalarField}; +use bouncycastle_ec::p256_sec1; +use bouncycastle_ec::p256_wnaf::shamir_multiply; +use bouncycastle_sha2::SHA256; +use bouncycastle_utils::secret::Secret; + +/// Raw `r || s` signature length: two 32-byte field-width integers, the default encoding. +/// `sign_der`/`verify_der` on the keys offer the RFC 3279 §2.2.3 +/// DER `SEQUENCE { r, s }` alternative via [`crate::der`], for interop that needs it. +pub const SIG_LEN: usize = 64; + +/// Upper bound on a DER-encoded P-256 signature (`r`, `s` each 32 bytes); see [`crate::der::max_len`]. +pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(32); + +/// ECDSA over P-256: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), fixed +/// to SHA-256. The algorithm marker the key role traits are implemented over: +/// [`ECDSAP256PrivateKey`] signs with it through [`SigningKey`] (a random per-message `k`, +/// §6.3.1 / Appendix A.3.1) and [`DeterministicSigningKey`] (`k` from `d` and the message hash, +/// §6.3.2 / Appendix A.3.3, RFC 6979); [`ECDSAP256PublicKey`] verifies through [`VerifyingKey`]. +/// §6 says the choice of `k` "does not impact the signature verification process", so there is +/// one verifier and a signature from either signer verifies under it. +/// +/// Neither signer uses `ctx`: ECDSA has no context string, and the argument is accepted and +/// ignored so that the key traits read the same across algorithms. +pub struct ECDSAP256; + +impl Algorithm for ECDSAP256 { + const ALG_NAME: &'static str = "ECDSA-P256"; + /// SP 800-57 Part 1 Rev. 5, Table 2: a 256-bit curve order gives 128 bits. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl SignatureAlgorithm for ECDSAP256 {} + +/// What [`ECDetDSAP256Signer`] reports as its algorithm name. The algorithm marker is still +/// [`ECDSAP256`]; this names the RFC 6979 way of choosing `k`. +pub const ECDETDSA_P256_NAME: &str = "ECDetDSA-P256"; + +/// The signer [`SigningKey::create_signer`] hands out: ECDSA with a random `k`. +/// +/// `k` is drawn from the caller's RNG when the signer is created (Appendix A.3.1, "extra random +/// bits"), since it does not depend on the message; the signer then only hashes. Two signatures +/// of one message under one key differ. +pub struct ECDSAP256Signer<'k> { + sk: &'k ECDSAP256PrivateKey, + hash: SHA256, + k: P256Scalar, +} + +/// The signer [`DeterministicSigningKey::create_signer_deterministic`] hands out: Deterministic +/// ECDSA, `k` from `d` and the message hash by RFC 6979, so the same key and message always give +/// the same signature and signing draws no randomness. Reports [`ECDETDSA_P256_NAME`]. +pub struct ECDetDSAP256Signer<'k> { + sk: &'k ECDSAP256PrivateKey, + hash: SHA256, +} + +/// The verifier [`VerifyingKey::create_verifier`] hands out; FIPS 186-5 §6.4.2. +pub struct ECDSAP256Verifier<'k> { + pk: &'k ECDSAP256PublicKey, + hash: SHA256, +} + +/// `x * z^-2`, branch-free -- see the module docs for why this isn't [`P256JacobianPoint::to_affine`]. +fn x_affine_of_signing_point(point: &P256JacobianPoint) -> P256FieldElement { + let z_inv = point.z.invert(); + point.x.mul(&z_inv.mul(&z_inv)) +} + +/// FIPS 186-5 §6.4.1 steps 4-12, given `k` already generated (step 3) by one of Appendix A.3's +/// methods. +/// Returns `None` for step 11's `r = 0` or `s = 0` case; each caller acts on it as step 11 +/// directs for its way of generating `k`. +fn sign_with_k( + sk: &ECDSAP256PrivateKey, + e: &P256ScalarField, + k: P256Scalar, +) -> Option<[u8; SIG_LEN]> { + let mut k_field = P256ScalarField::from_secret(&k); + let mut k_inv = k_field.invert(); // step 4 + + let r_point = comb_multiply_base_point(&k); // step 5 + let x_r = x_affine_of_signing_point(&r_point); // steps 6-7 + let r = P256ScalarField::from_limbs(x_r.to_limbs()); // step 8 + + let mut d_field = P256ScalarField::from_secret(sk.scalar()); + let s = k_inv.mul(&e.add(&r.mul(&d_field))); // step 9 + + // `k_field`, `k_inv` and `d_field` hold `k`, `k^-1` and `d`. Nothing below needs them, so they + // are overwritten here rather than left legible on the stack after this returns -- placed + // before step 11's check so that both ways out of the function are covered. `k` itself is a + // `Secret` and scrubs when it drops; `e`, `r` and `s` are public by construction. + k_field.zeroize(); + k_inv.zeroize(); + d_field.zeroize(); + + // step 11: astronomically unlikely (r, s range over ~2^256 values each); this comparison is + // FIPS 186-5's own mandated success/failure branch on the just-computed public output, not a + // constant-time violation -- see this module's docs on x_affine_of_signing_point for the one + // secret-derived branch this function does avoid. + if r_or_s_is_zero(&r, &s) { + return None; + } + + let mut sig = [0u8; SIG_LEN]; + sig[..32].copy_from_slice(&p256_sec1::be_bytes_from_limbs(&r.to_limbs())); + sig[32..].copy_from_slice(&p256_sec1::be_bytes_from_limbs(&s.to_limbs())); + Some(sig) +} + +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1: `N + 64` bits from `rng`, reduced into `[1, n-1]` +/// by `reduce_wide_bits_mod_n_minus_1`. The one place [`ECDSAP256`]'s signing paths draw `k`. +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + +/// `e` from a 32-byte message hash: FIPS 186-5 §6.4.1 step 2 / §6.4.2 step 3, `E = H` (no +/// truncation needed -- see the module docs) then Appendix B.2.1's big-endian conversion. +fn e_from_hash(h: &[u8; 32]) -> P256ScalarField { + P256ScalarField::from_limbs(p256_sec1::limbs_from_be_bytes(h)) +} + +/// FIPS 186-5 §6.4.1 step 11's `r = 0` or `s = 0` check, named so it has something to unit test +/// directly: reaching either branch through `sign_with_k` needs an `r`/`s` value that is +/// astronomically unlikely for any real `k`/`d`/message (see [`sign_with_k`]'s call site). +fn r_or_s_is_zero(r: &P256ScalarField, s: &P256ScalarField) -> bool { + *r == P256ScalarField::ZERO || *s == P256ScalarField::ZERO +} + +/// FIPS 186-5 §6.4.2 on the finished message hash: step 1's range check, then steps 3-9 (step 2 +/// is the hashing the caller has done). Both signer types' `do_verify_final` are this one +/// function, since how `k` was generated does not enter verification. +fn verify_final(hash: SHA256, pk: &ECDSAP256PublicKey, sig: &[u8]) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::LengthError("ECDSA P-256 signature must be exactly 64 bytes")); + } + + // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. + let r_limbs = p256_sec1::limbs_from_be_bytes(&sig[..32].try_into().unwrap()); + let s_limbs = p256_sec1::limbs_from_be_bytes(&sig[32..64].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = P256ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 32]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); // steps 2-3 + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = P256PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 + let u = e.mul(&s_inv); // step 5 + let v = r.mul(&s_inv); + + let q = P256JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = P256PublicScalar::from_limbs(u.to_limbs()); + let v_pub = P256PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = P256ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; unused trailing +/// bytes of the fixed-size buffer are left as written by [`crate::der::encode`] (zeroed only up +/// to that length). +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..32], &raw[32..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(32); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + +impl ECDSAP256PrivateKey { + /// [`SigningKey::sign`] with the signature DER-encoded as RFC 3279 §2.2.3's + /// `SEQUENCE { r, s }` (see [`crate::der`]); returns the buffer and the encoded length. + pub fn sign_der( + &self, + rng: &mut dyn RNG, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign(rng, msg, ctx)?)) + } + + /// [`DeterministicSigningKey::sign_deterministic`], DER-encoded as [`Self::sign_der`]. + pub fn sign_der_deterministic( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign_deterministic(msg, ctx)?)) + } +} + +impl ECDSAP256PublicKey { + /// [`VerifyingKey::verify`] of a DER-encoded signature (RFC 3279 §2.2.3); a malformed + /// encoding is a verification failure. + pub fn verify_der( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + self.verify(msg, ctx, &der_decode(sig)?) + } +} + +impl SigningKey for ECDSAP256PrivateKey { + type Signer<'k> + = ECDSAP256Signer<'k> + where + Self: 'k; + + fn create_signer<'k>( + &'k self, + rng: &mut dyn RNG, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSAP256Signer { sk: self, hash: SHA256::default(), k: random_k(rng)? }) + } +} + +impl DeterministicSigningKey for ECDSAP256PrivateKey { + type Signer<'k> + = ECDetDSAP256Signer<'k> + where + Self: 'k; + + fn create_signer_deterministic<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDetDSAP256Signer { sk: self, hash: SHA256::default() }) + } +} + +impl VerifyingKey for ECDSAP256PublicKey { + type Verifier<'k> + = ECDSAP256Verifier<'k> + where + Self: 'k; + + fn create_verifier<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSAP256Verifier { pk: self, hash: SHA256::default() }) + } +} + +impl AlgorithmInfo for ECDSAP256Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDSAP256::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSAP256::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDSAP256Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 32]; + self.hash.do_final_out(&mut h); + // FIPS 186-5 §6.4.1 step 11: with a random `k`, `r = 0` or `s = 0` goes back to step 3. + // `k` was drawn when this signer was created and there is no RNG here to draw another + // from, so the failure is reported instead; it has probability about 2^-255. + sign_with_k(self.sk, &e_from_hash(&h), self.k).ok_or(SignatureError::GenericError( + "ECDSA produced r = 0 or s = 0; create a fresh signer and sign again", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDetDSAP256Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDETDSA_P256_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSAP256::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDetDSAP256Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 32]; + self.hash.do_final_out(&mut h); + let k = rfc6979_p256::generate_k(self.sk.scalar(), &h); // step 3, Appendix A.3.3 + // FIPS 186-5 §6.4.1 step 11: with a deterministic `k`, `r = 0` or `s = 0` outputs failure. + sign_with_k(self.sk, &e_from_hash(&h), k).ok_or(SignatureError::GenericError( + "deterministic ECDSA produced r = 0 or s = 0; FIPS 186-5 §6.4.1 step 11: output failure", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDSAP256Verifier<'_> { + fn alg_name(&self) -> &'static str { + ECDSAP256::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSAP256::MAX_SECURITY_STRENGTH + } +} + +impl SignatureVerifier for ECDSAP256Verifier<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + verify_final(self.hash, self.pk, sig) + } +} + +fn in_range(limbs: &[u64; 4]) -> bool { + if *limbs == [0, 0, 0, 0] { + return false; + } + let (_, borrow) = nat::sub(limbs, &N_LIMBS); + borrow == 1 +} + +// `r_or_s_is_zero` is private and, through `sign_with_k`, only ever reached with an `r`/`s` value +// that is astronomically unlikely for any real input (see its own doc comment), so no realistic +// integration test exercises either disjunct -- the QUALITY_AND_STYLE.md private-function +// exception applies. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn r_or_s_is_zero_requires_only_one_to_be_zero() { + let zero = P256ScalarField::ZERO; + let one = P256ScalarField::ONE; + assert!(!r_or_s_is_zero(&one, &one)); + assert!(r_or_s_is_zero(&zero, &one)); + assert!(r_or_s_is_zero(&one, &zero)); + assert!(r_or_s_is_zero(&zero, &zero)); + } +} diff --git a/crypto/ecdsa/src/ecdsa_p256k1.rs b/crypto/ecdsa/src/ecdsa_p256k1.rs new file mode 100644 index 000000000..13d68f1ac --- /dev/null +++ b/crypto/ecdsa/src/ecdsa_p256k1.rs @@ -0,0 +1,409 @@ +//! ECDSA over secp256k1: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), +//! fixed to SHA-256. [`ECDSASecp256K1`] is the algorithm marker; [`ECDSASecp256K1PrivateKey`] signs +//! through [`SigningKey`] (random `k`, §6.3.1) and [`DeterministicSigningKey`] (`k` by RFC 6979, +//! §6.3.2), [`ECDSASecp256K1PublicKey`] verifies through [`VerifyingKey`], as [`crate::ecdsa_p256`] +//! lays out. Fixed to SHA-256. Identical in shape to [`crate::ecdsa_p256`] -- see that module's docs for the +//! full reasoning (why `e` needs no truncation, why `x_affine_of_signing_point` exists, the +//! `ctx`/ignore-it stance) -- with secp256k1's types substituted. + +use crate::keys_p256k1::{ + ECDSASecp256K1PrivateKey, ECDSASecp256K1PublicKey, EXTRA_BITS_DRBG_OUTPUT_LEN, +}; +use crate::rfc6979_p256k1; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::signature_key::{ + DeterministicSigningKey, SignatureAlgorithm, SigningKey, VerifyingKey, +}; +use bouncycastle_core::traits::{ + Algorithm, AlgorithmInfo, Hash, RNG, SignatureSigner, SignatureVerifier, +}; +use bouncycastle_ec::nat; +use bouncycastle_ec::p256k1::P256K1FieldElement; +use bouncycastle_ec::p256k1_comb::comb_multiply_base_point; +use bouncycastle_ec::p256k1_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::p256k1_point::P256K1JacobianPoint; +use bouncycastle_ec::p256k1_scalar::{ + N_LIMBS, P256K1PublicScalar, P256K1Scalar, P256K1ScalarField, +}; +use bouncycastle_ec::p256k1_sec1; +use bouncycastle_ec::p256k1_wnaf::shamir_multiply; +use bouncycastle_sha2::SHA256; +use bouncycastle_utils::secret::Secret; + +/// Raw `r || s` signature length: two 32-byte field-width integers. +pub const SIG_LEN: usize = 64; + +/// Upper bound on a DER-encoded secp256k1 signature (`r`, `s` each 32 bytes); see +/// [`crate::der::max_len`]. +pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(32); + +/// ECDSA over secp256k1: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), fixed +/// to SHA-256. The algorithm marker the key role traits are implemented over: +/// [`ECDSASecp256K1PrivateKey`] signs with it through [`SigningKey`] (a random per-message `k`, +/// §6.3.1 / Appendix A.3.1) and [`DeterministicSigningKey`] (`k` from `d` and the message hash, +/// §6.3.2 / Appendix A.3.3, RFC 6979); [`ECDSASecp256K1PublicKey`] verifies through [`VerifyingKey`]. +/// §6 says the choice of `k` "does not impact the signature verification process", so there is +/// one verifier and a signature from either signer verifies under it. +/// +/// Neither signer uses `ctx`: ECDSA has no context string, and the argument is accepted and +/// ignored so that the key traits read the same across algorithms. +pub struct ECDSASecp256K1; + +impl Algorithm for ECDSASecp256K1 { + const ALG_NAME: &'static str = "ECDSA-secp256k1"; + /// SP 800-57 Part 1 Rev. 5, Table 2: a 256-bit curve order gives 128 bits. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl SignatureAlgorithm for ECDSASecp256K1 {} + +/// What [`ECDetDSASecp256K1Signer`] reports as its algorithm name. The algorithm marker is still +/// [`ECDSASecp256K1`]; this names the RFC 6979 way of choosing `k`. +pub const ECDETDSA_SECP256K1_NAME: &str = "ECDetDSA-secp256k1"; + +/// The signer [`SigningKey::create_signer`] hands out: ECDSA with a random `k`. +/// +/// `k` is drawn from the caller's RNG when the signer is created (Appendix A.3.1, "extra random +/// bits"), since it does not depend on the message; the signer then only hashes. Two signatures +/// of one message under one key differ. +pub struct ECDSASecp256K1Signer<'k> { + sk: &'k ECDSASecp256K1PrivateKey, + hash: SHA256, + k: P256K1Scalar, +} + +/// The signer [`DeterministicSigningKey::create_signer_deterministic`] hands out: Deterministic +/// ECDSA, `k` from `d` and the message hash by RFC 6979, so the same key and message always give +/// the same signature and signing draws no randomness. Reports [`ECDETDSA_SECP256K1_NAME`]. +pub struct ECDetDSASecp256K1Signer<'k> { + sk: &'k ECDSASecp256K1PrivateKey, + hash: SHA256, +} + +/// The verifier [`VerifyingKey::create_verifier`] hands out; FIPS 186-5 §6.4.2. +pub struct ECDSASecp256K1Verifier<'k> { + pk: &'k ECDSASecp256K1PublicKey, + hash: SHA256, +} + +/// `x * z^-2`, branch-free. See [`crate::ecdsa_p256::x_affine_of_signing_point`]'s docs. +fn x_affine_of_signing_point(point: &P256K1JacobianPoint) -> P256K1FieldElement { + let z_inv = point.z.invert(); + point.x.mul(&z_inv.mul(&z_inv)) +} + +/// FIPS 186-5 §6.4.1 steps 4-12, given `k` already generated (step 3). +/// Returns `None` for step 11's `r = 0` or `s = 0` case; each caller acts on it as step 11 +/// directs for its way of generating `k`. +fn sign_with_k( + sk: &ECDSASecp256K1PrivateKey, + e: &P256K1ScalarField, + k: P256K1Scalar, +) -> Option<[u8; SIG_LEN]> { + let mut k_field = P256K1ScalarField::from_secret(&k); + let mut k_inv = k_field.invert(); // step 4 + + let r_point = comb_multiply_base_point(&k); // step 5 + let x_r = x_affine_of_signing_point(&r_point); // steps 6-7 + let r = P256K1ScalarField::from_limbs(x_r.to_limbs()); // step 8 + + let mut d_field = P256K1ScalarField::from_secret(sk.scalar()); + let s = k_inv.mul(&e.add(&r.mul(&d_field))); // step 9 + + // `k_field`, `k_inv` and `d_field` hold `k`, `k^-1` and `d`. Nothing below needs them, so they + // are overwritten here rather than left legible on the stack after this returns -- placed + // before step 11's check so that both ways out of the function are covered. `k` itself is a + // `Secret` and scrubs when it drops; `e`, `r` and `s` are public by construction. + k_field.zeroize(); + k_inv.zeroize(); + d_field.zeroize(); + + if r_or_s_is_zero(&r, &s) { + return None; + } + + let mut sig = [0u8; SIG_LEN]; + sig[..32].copy_from_slice(&p256k1_sec1::be_bytes_from_limbs(&r.to_limbs())); + sig[32..].copy_from_slice(&p256k1_sec1::be_bytes_from_limbs(&s.to_limbs())); + Some(sig) +} + +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1, reduced into `[1, n-1]` by +/// `reduce_wide_bits_mod_n_minus_1`; the one place [`ECDSASecp256K1`]'s signing paths draw `k`. +/// Draws A.3.1 step 3's `N + t` bits with `t = 64` (see [`bouncycastle_ec::p256k1_extra_bits`]'s docs for why +/// that is required here even though secp256k1's `n` would need none for bias alone). +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + +/// `e` from a 32-byte message hash: `E = H` (no truncation needed, `len(n) == hashlen`), then +/// Appendix B.2.1's big-endian conversion. +fn e_from_hash(h: &[u8; 32]) -> P256K1ScalarField { + P256K1ScalarField::from_limbs(p256k1_sec1::limbs_from_be_bytes(h)) +} + +/// FIPS 186-5 §6.4.1 step 11's `r = 0` or `s = 0` check. See +/// [`crate::ecdsa_p256::r_or_s_is_zero`]'s docs. +fn r_or_s_is_zero(r: &P256K1ScalarField, s: &P256K1ScalarField) -> bool { + *r == P256K1ScalarField::ZERO || *s == P256K1ScalarField::ZERO +} + +/// FIPS 186-5 §6.4.2 on the finished message hash; the verifier's `do_verify_final` is +/// this one function. See `crate::ecdsa_p256`'s `verify_final`. +fn verify_final( + hash: SHA256, + pk: &ECDSASecp256K1PublicKey, + sig: &[u8], +) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::LengthError( + "ECDSA secp256k1 signature must be exactly 64 bytes", + )); + } + + // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. + let r_limbs = p256k1_sec1::limbs_from_be_bytes(&sig[..32].try_into().unwrap()); + let s_limbs = p256k1_sec1::limbs_from_be_bytes(&sig[32..64].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = P256K1ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 32]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); // steps 2-3 + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = P256K1PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 + let u = e.mul(&s_inv); // step 5 + let v = r.mul(&s_inv); + + let q = P256K1JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = P256K1PublicScalar::from_limbs(u.to_limbs()); + let v_pub = P256K1PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = P256K1ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; see +/// `crate::ecdsa_p256`'s `der_encode`. +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..32], &raw[32..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(32); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + +impl ECDSASecp256K1PrivateKey { + /// [`SigningKey::sign`] with the signature DER-encoded as RFC 3279 §2.2.3's + /// `SEQUENCE { r, s }` (see [`crate::der`]); returns the buffer and the encoded length. + pub fn sign_der( + &self, + rng: &mut dyn RNG, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign(rng, msg, ctx)?)) + } + + /// [`DeterministicSigningKey::sign_deterministic`], DER-encoded as [`Self::sign_der`]. + pub fn sign_der_deterministic( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign_deterministic(msg, ctx)?)) + } +} + +impl ECDSASecp256K1PublicKey { + /// [`VerifyingKey::verify`] of a DER-encoded signature (RFC 3279 §2.2.3); a malformed + /// encoding is a verification failure. + pub fn verify_der( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + self.verify(msg, ctx, &der_decode(sig)?) + } +} + +impl SigningKey for ECDSASecp256K1PrivateKey { + type Signer<'k> + = ECDSASecp256K1Signer<'k> + where + Self: 'k; + + fn create_signer<'k>( + &'k self, + rng: &mut dyn RNG, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSASecp256K1Signer { sk: self, hash: SHA256::default(), k: random_k(rng)? }) + } +} + +impl DeterministicSigningKey for ECDSASecp256K1PrivateKey { + type Signer<'k> + = ECDetDSASecp256K1Signer<'k> + where + Self: 'k; + + fn create_signer_deterministic<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDetDSASecp256K1Signer { sk: self, hash: SHA256::default() }) + } +} + +impl VerifyingKey for ECDSASecp256K1PublicKey { + type Verifier<'k> + = ECDSASecp256K1Verifier<'k> + where + Self: 'k; + + fn create_verifier<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSASecp256K1Verifier { pk: self, hash: SHA256::default() }) + } +} + +impl AlgorithmInfo for ECDSASecp256K1Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDSASecp256K1::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSASecp256K1::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDSASecp256K1Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 32]; + self.hash.do_final_out(&mut h); + // FIPS 186-5 §6.4.1 step 11: with a random `k`, `r = 0` or `s = 0` goes back to step 3. + // `k` was drawn when this signer was created and there is no RNG here to draw another + // from, so the failure is reported instead; it has probability about 2^-255. + sign_with_k(self.sk, &e_from_hash(&h), self.k).ok_or(SignatureError::GenericError( + "ECDSA produced r = 0 or s = 0; create a fresh signer and sign again", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDetDSASecp256K1Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDETDSA_SECP256K1_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSASecp256K1::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDetDSASecp256K1Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 32]; + self.hash.do_final_out(&mut h); + let k = rfc6979_p256k1::generate_k(self.sk.scalar(), &h); // step 3, Appendix A.3.3 + // FIPS 186-5 §6.4.1 step 11: with a deterministic `k`, `r = 0` or `s = 0` outputs failure. + sign_with_k(self.sk, &e_from_hash(&h), k).ok_or(SignatureError::GenericError( + "deterministic ECDSA produced r = 0 or s = 0; FIPS 186-5 §6.4.1 step 11: output failure", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDSASecp256K1Verifier<'_> { + fn alg_name(&self) -> &'static str { + ECDSASecp256K1::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSASecp256K1::MAX_SECURITY_STRENGTH + } +} + +impl SignatureVerifier for ECDSASecp256K1Verifier<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + verify_final(self.hash, self.pk, sig) + } +} + +fn in_range(limbs: &[u64; 4]) -> bool { + if *limbs == [0, 0, 0, 0] { + return false; + } + let (_, borrow) = nat::sub(limbs, &N_LIMBS); + borrow == 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn r_or_s_is_zero_requires_only_one_to_be_zero() { + let zero = P256K1ScalarField::ZERO; + let one = P256K1ScalarField::ONE; + assert!(!r_or_s_is_zero(&one, &one)); + assert!(r_or_s_is_zero(&zero, &one)); + assert!(r_or_s_is_zero(&one, &zero)); + assert!(r_or_s_is_zero(&zero, &zero)); + } +} diff --git a/crypto/ecdsa/src/ecdsa_p384.rs b/crypto/ecdsa/src/ecdsa_p384.rs new file mode 100644 index 000000000..10efd7aa0 --- /dev/null +++ b/crypto/ecdsa/src/ecdsa_p384.rs @@ -0,0 +1,398 @@ +//! ECDSA over P-384: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), +//! fixed to SHA-384. [`ECDSAP384`] is the algorithm marker; [`ECDSAP384PrivateKey`] signs +//! through [`SigningKey`] (random `k`, §6.3.1) and [`DeterministicSigningKey`] (`k` by RFC 6979, +//! §6.3.2), [`ECDSAP384PublicKey`] verifies through [`VerifyingKey`], as [`crate::ecdsa_p256`] +//! lays out. Fixed to SHA-384. +//! Identical in shape to [`crate::ecdsa_p256`] -- see that module's docs for the full reasoning +//! (why `e` needs no truncation, why `x_affine_of_signing_point` exists, the `ctx`/ignore-it +//! stance) -- with P-384's types and SHA-384 substituted. + +use crate::keys_p384::{ECDSAP384PrivateKey, ECDSAP384PublicKey, EXTRA_BITS_DRBG_OUTPUT_LEN}; +use crate::rfc6979_p384; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::signature_key::{ + DeterministicSigningKey, SignatureAlgorithm, SigningKey, VerifyingKey, +}; +use bouncycastle_core::traits::{ + Algorithm, AlgorithmInfo, Hash, RNG, SignatureSigner, SignatureVerifier, +}; +use bouncycastle_ec::nat; +use bouncycastle_ec::p384::P384FieldElement; +use bouncycastle_ec::p384_comb::comb_multiply_base_point; +use bouncycastle_ec::p384_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::p384_point::P384JacobianPoint; +use bouncycastle_ec::p384_scalar::{N_LIMBS, P384PublicScalar, P384Scalar, P384ScalarField}; +use bouncycastle_ec::p384_sec1; +use bouncycastle_ec::p384_wnaf::shamir_multiply; +use bouncycastle_sha2::SHA384; +use bouncycastle_utils::secret::Secret; + +/// Raw `r || s` signature length: two 48-byte field-width integers. +pub const SIG_LEN: usize = 96; + +/// Upper bound on a DER-encoded P-384 signature (`r`, `s` each 48 bytes); see [`crate::der::max_len`]. +pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(48); + +/// ECDSA over P-384: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), fixed +/// to SHA-384. The algorithm marker the key role traits are implemented over: +/// [`ECDSAP384PrivateKey`] signs with it through [`SigningKey`] (a random per-message `k`, +/// §6.3.1 / Appendix A.3.1) and [`DeterministicSigningKey`] (`k` from `d` and the message hash, +/// §6.3.2 / Appendix A.3.3, RFC 6979); [`ECDSAP384PublicKey`] verifies through [`VerifyingKey`]. +/// §6 says the choice of `k` "does not impact the signature verification process", so there is +/// one verifier and a signature from either signer verifies under it. +/// +/// Neither signer uses `ctx`: ECDSA has no context string, and the argument is accepted and +/// ignored so that the key traits read the same across algorithms. +pub struct ECDSAP384; + +impl Algorithm for ECDSAP384 { + const ALG_NAME: &'static str = "ECDSA-P384"; + /// SP 800-57 Part 1 Rev. 5, Table 2: a 384-bit curve order gives 192 bits. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; +} + +impl SignatureAlgorithm for ECDSAP384 {} + +/// What [`ECDetDSAP384Signer`] reports as its algorithm name. The algorithm marker is still +/// [`ECDSAP384`]; this names the RFC 6979 way of choosing `k`. +pub const ECDETDSA_P384_NAME: &str = "ECDetDSA-P384"; + +/// The signer [`SigningKey::create_signer`] hands out: ECDSA with a random `k`. +/// +/// `k` is drawn from the caller's RNG when the signer is created (Appendix A.3.1, "extra random +/// bits"), since it does not depend on the message; the signer then only hashes. Two signatures +/// of one message under one key differ. +pub struct ECDSAP384Signer<'k> { + sk: &'k ECDSAP384PrivateKey, + hash: SHA384, + k: P384Scalar, +} + +/// The signer [`DeterministicSigningKey::create_signer_deterministic`] hands out: Deterministic +/// ECDSA, `k` from `d` and the message hash by RFC 6979, so the same key and message always give +/// the same signature and signing draws no randomness. Reports [`ECDETDSA_P384_NAME`]. +pub struct ECDetDSAP384Signer<'k> { + sk: &'k ECDSAP384PrivateKey, + hash: SHA384, +} + +/// The verifier [`VerifyingKey::create_verifier`] hands out; FIPS 186-5 §6.4.2. +pub struct ECDSAP384Verifier<'k> { + pk: &'k ECDSAP384PublicKey, + hash: SHA384, +} + +/// `x * z^-2`, branch-free. See [`crate::ecdsa_p256::x_affine_of_signing_point`]'s docs. +fn x_affine_of_signing_point(point: &P384JacobianPoint) -> P384FieldElement { + let z_inv = point.z.invert(); + point.x.mul(&z_inv.mul(&z_inv)) +} + +/// FIPS 186-5 §6.4.1 steps 4-12, given `k` already generated (step 3). +/// Returns `None` for step 11's `r = 0` or `s = 0` case; each caller acts on it as step 11 +/// directs for its way of generating `k`. +fn sign_with_k( + sk: &ECDSAP384PrivateKey, + e: &P384ScalarField, + k: P384Scalar, +) -> Option<[u8; SIG_LEN]> { + let mut k_field = P384ScalarField::from_secret(&k); + let mut k_inv = k_field.invert(); // step 4 + + let r_point = comb_multiply_base_point(&k); // step 5 + let x_r = x_affine_of_signing_point(&r_point); // steps 6-7 + let r = P384ScalarField::from_limbs(x_r.to_limbs()); // step 8 + + let mut d_field = P384ScalarField::from_secret(sk.scalar()); + let s = k_inv.mul(&e.add(&r.mul(&d_field))); // step 9 + + // `k_field`, `k_inv` and `d_field` hold `k`, `k^-1` and `d`. Nothing below needs them, so they + // are overwritten here rather than left legible on the stack after this returns -- placed + // before step 11's check so that both ways out of the function are covered. `k` itself is a + // `Secret` and scrubs when it drops; `e`, `r` and `s` are public by construction. + k_field.zeroize(); + k_inv.zeroize(); + d_field.zeroize(); + + if r_or_s_is_zero(&r, &s) { + return None; + } + + let mut sig = [0u8; SIG_LEN]; + sig[..48].copy_from_slice(&p384_sec1::be_bytes_from_limbs(&r.to_limbs())); + sig[48..].copy_from_slice(&p384_sec1::be_bytes_from_limbs(&s.to_limbs())); + Some(sig) +} + +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1, reduced into `[1, n-1]` by +/// `reduce_wide_bits_mod_n_minus_1`; the one place [`ECDSAP384`]'s signing paths draw `k`. Draws +/// A.3.1 step 3's `N + t` bits with `t = 64` (see [`bouncycastle_ec::p384_extra_bits`]'s docs for why that is +/// required here even though P-384's `n` would need none for bias alone). +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + +/// `e` from a 48-byte message hash: `E = H` (no truncation needed, `len(n) == hashlen`), then +/// Appendix B.2.1's big-endian conversion. +fn e_from_hash(h: &[u8; 48]) -> P384ScalarField { + P384ScalarField::from_limbs(p384_sec1::limbs_from_be_bytes(h)) +} + +/// FIPS 186-5 §6.4.1 step 11's `r = 0` or `s = 0` check. See +/// [`crate::ecdsa_p256::r_or_s_is_zero`]'s docs. +fn r_or_s_is_zero(r: &P384ScalarField, s: &P384ScalarField) -> bool { + *r == P384ScalarField::ZERO || *s == P384ScalarField::ZERO +} + +/// FIPS 186-5 §6.4.2 on the finished message hash; the verifier's `do_verify_final` is +/// this one function. See `crate::ecdsa_p256`'s `verify_final`. +fn verify_final(hash: SHA384, pk: &ECDSAP384PublicKey, sig: &[u8]) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::LengthError("ECDSA P-384 signature must be exactly 96 bytes")); + } + + let r_limbs = p384_sec1::limbs_from_be_bytes(&sig[..48].try_into().unwrap()); + let s_limbs = p384_sec1::limbs_from_be_bytes(&sig[48..96].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = P384ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 48]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = P384PublicScalar::from_limbs(s_limbs).invert_vartime(); + let u = e.mul(&s_inv); + let v = r.mul(&s_inv); + + let q = P384JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = P384PublicScalar::from_limbs(u.to_limbs()); + let v_pub = P384PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = P384ScalarField::from_limbs(x_r1.to_limbs()); + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; see +/// `crate::ecdsa_p256`'s `der_encode`. +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..48], &raw[48..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(48); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + +impl ECDSAP384PrivateKey { + /// [`SigningKey::sign`] with the signature DER-encoded as RFC 3279 §2.2.3's + /// `SEQUENCE { r, s }` (see [`crate::der`]); returns the buffer and the encoded length. + pub fn sign_der( + &self, + rng: &mut dyn RNG, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign(rng, msg, ctx)?)) + } + + /// [`DeterministicSigningKey::sign_deterministic`], DER-encoded as [`Self::sign_der`]. + pub fn sign_der_deterministic( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign_deterministic(msg, ctx)?)) + } +} + +impl ECDSAP384PublicKey { + /// [`VerifyingKey::verify`] of a DER-encoded signature (RFC 3279 §2.2.3); a malformed + /// encoding is a verification failure. + pub fn verify_der( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + self.verify(msg, ctx, &der_decode(sig)?) + } +} + +impl SigningKey for ECDSAP384PrivateKey { + type Signer<'k> + = ECDSAP384Signer<'k> + where + Self: 'k; + + fn create_signer<'k>( + &'k self, + rng: &mut dyn RNG, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSAP384Signer { sk: self, hash: SHA384::default(), k: random_k(rng)? }) + } +} + +impl DeterministicSigningKey for ECDSAP384PrivateKey { + type Signer<'k> + = ECDetDSAP384Signer<'k> + where + Self: 'k; + + fn create_signer_deterministic<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDetDSAP384Signer { sk: self, hash: SHA384::default() }) + } +} + +impl VerifyingKey for ECDSAP384PublicKey { + type Verifier<'k> + = ECDSAP384Verifier<'k> + where + Self: 'k; + + fn create_verifier<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSAP384Verifier { pk: self, hash: SHA384::default() }) + } +} + +impl AlgorithmInfo for ECDSAP384Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDSAP384::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSAP384::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDSAP384Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 48]; + self.hash.do_final_out(&mut h); + // FIPS 186-5 §6.4.1 step 11: with a random `k`, `r = 0` or `s = 0` goes back to step 3. + // `k` was drawn when this signer was created and there is no RNG here to draw another + // from, so the failure is reported instead; it has probability about 2^-383. + sign_with_k(self.sk, &e_from_hash(&h), self.k).ok_or(SignatureError::GenericError( + "ECDSA produced r = 0 or s = 0; create a fresh signer and sign again", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDetDSAP384Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDETDSA_P384_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSAP384::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDetDSAP384Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 48]; + self.hash.do_final_out(&mut h); + let k = rfc6979_p384::generate_k(self.sk.scalar(), &h); // step 3, Appendix A.3.3 + // FIPS 186-5 §6.4.1 step 11: with a deterministic `k`, `r = 0` or `s = 0` outputs failure. + sign_with_k(self.sk, &e_from_hash(&h), k).ok_or(SignatureError::GenericError( + "deterministic ECDSA produced r = 0 or s = 0; FIPS 186-5 §6.4.1 step 11: output failure", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDSAP384Verifier<'_> { + fn alg_name(&self) -> &'static str { + ECDSAP384::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSAP384::MAX_SECURITY_STRENGTH + } +} + +impl SignatureVerifier for ECDSAP384Verifier<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + verify_final(self.hash, self.pk, sig) + } +} + +fn in_range(limbs: &[u64; 6]) -> bool { + if *limbs == [0, 0, 0, 0, 0, 0] { + return false; + } + let (_, borrow) = nat::sub(limbs, &N_LIMBS); + borrow == 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn r_or_s_is_zero_requires_only_one_to_be_zero() { + let zero = P384ScalarField::ZERO; + let one = P384ScalarField::ONE; + assert!(!r_or_s_is_zero(&one, &one)); + assert!(r_or_s_is_zero(&zero, &one)); + assert!(r_or_s_is_zero(&one, &zero)); + assert!(r_or_s_is_zero(&zero, &zero)); + } +} diff --git a/crypto/ecdsa/src/ecdsa_p521.rs b/crypto/ecdsa/src/ecdsa_p521.rs new file mode 100644 index 000000000..6e26be2c9 --- /dev/null +++ b/crypto/ecdsa/src/ecdsa_p521.rs @@ -0,0 +1,403 @@ +//! ECDSA over P-521: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), +//! fixed to SHA-512. [`ECDSAP521`] is the algorithm marker; [`ECDSAP521PrivateKey`] signs +//! through [`SigningKey`] (random `k`, §6.3.1) and [`DeterministicSigningKey`] (`k` by RFC 6979, +//! §6.3.2), [`ECDSAP521PublicKey`] verifies through [`VerifyingKey`], as [`crate::ecdsa_p256`] +//! lays out. Fixed to SHA-512. +//! Identical in shape to [`crate::ecdsa_p384`] -- see [`crate::ecdsa_p256`]'s docs for the shared +//! reasoning (why `x_affine_of_signing_point` exists, the `ctx`/ignore-it stance) -- with P-521's +//! types, SHA-512, and one difference `e_from_hash` documents: unlike P-256/SHA-256 and +//! P-384/SHA-384, `len(n) = 521` and `hashlen = 512` are not equal here, though `len(n) >= hashlen` +//! still holds, so `E = H` still needs no truncation (FIPS 186-5 §6.4.1 step 2a). + +use crate::keys_p521::{ECDSAP521PrivateKey, ECDSAP521PublicKey, EXTRA_BITS_DRBG_OUTPUT_LEN}; +use crate::rfc6979_p521; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::signature_key::{ + DeterministicSigningKey, SignatureAlgorithm, SigningKey, VerifyingKey, +}; +use bouncycastle_core::traits::{ + Algorithm, AlgorithmInfo, Hash, RNG, SignatureSigner, SignatureVerifier, +}; +use bouncycastle_ec::nat; +use bouncycastle_ec::p521::P521FieldElement; +use bouncycastle_ec::p521_comb::comb_multiply_base_point; +use bouncycastle_ec::p521_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::p521_point::P521JacobianPoint; +use bouncycastle_ec::p521_scalar::{N_LIMBS, P521PublicScalar, P521Scalar, P521ScalarField}; +use bouncycastle_ec::p521_sec1; +use bouncycastle_ec::p521_wnaf::shamir_multiply; +use bouncycastle_sha2::SHA512; +use bouncycastle_utils::secret::Secret; + +/// Raw `r || s` signature length: two 66-byte field-width integers. +pub const SIG_LEN: usize = 132; + +/// Upper bound on a DER-encoded P-521 signature (`r`, `s` each 66 bytes); see [`crate::der::max_len`]. +pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(66); + +/// ECDSA over P-521: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 (verification), fixed +/// to SHA-512. The algorithm marker the key role traits are implemented over: +/// [`ECDSAP521PrivateKey`] signs with it through [`SigningKey`] (a random per-message `k`, +/// §6.3.1 / Appendix A.3.1) and [`DeterministicSigningKey`] (`k` from `d` and the message hash, +/// §6.3.2 / Appendix A.3.3, RFC 6979); [`ECDSAP521PublicKey`] verifies through [`VerifyingKey`]. +/// §6 says the choice of `k` "does not impact the signature verification process", so there is +/// one verifier and a signature from either signer verifies under it. +/// +/// Neither signer uses `ctx`: ECDSA has no context string, and the argument is accepted and +/// ignored so that the key traits read the same across algorithms. +pub struct ECDSAP521; + +impl Algorithm for ECDSAP521 { + const ALG_NAME: &'static str = "ECDSA-P521"; + /// SP 800-57 Part 1 Rev. 5, Table 2: a 521-bit curve order gives 256 bits. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +} + +impl SignatureAlgorithm for ECDSAP521 {} + +/// What [`ECDetDSAP521Signer`] reports as its algorithm name. The algorithm marker is still +/// [`ECDSAP521`]; this names the RFC 6979 way of choosing `k`. +pub const ECDETDSA_P521_NAME: &str = "ECDetDSA-P521"; + +/// The signer [`SigningKey::create_signer`] hands out: ECDSA with a random `k`. +/// +/// `k` is drawn from the caller's RNG when the signer is created (Appendix A.3.1, "extra random +/// bits"), since it does not depend on the message; the signer then only hashes. Two signatures +/// of one message under one key differ. +pub struct ECDSAP521Signer<'k> { + sk: &'k ECDSAP521PrivateKey, + hash: SHA512, + k: P521Scalar, +} + +/// The signer [`DeterministicSigningKey::create_signer_deterministic`] hands out: Deterministic +/// ECDSA, `k` from `d` and the message hash by RFC 6979, so the same key and message always give +/// the same signature and signing draws no randomness. Reports [`ECDETDSA_P521_NAME`]. +pub struct ECDetDSAP521Signer<'k> { + sk: &'k ECDSAP521PrivateKey, + hash: SHA512, +} + +/// The verifier [`VerifyingKey::create_verifier`] hands out; FIPS 186-5 §6.4.2. +pub struct ECDSAP521Verifier<'k> { + pk: &'k ECDSAP521PublicKey, + hash: SHA512, +} + +/// `x * z^-2`, branch-free. See [`crate::ecdsa_p256::x_affine_of_signing_point`]'s docs. +fn x_affine_of_signing_point(point: &P521JacobianPoint) -> P521FieldElement { + let z_inv = point.z.invert(); + point.x.mul(&z_inv.mul(&z_inv)) +} + +/// FIPS 186-5 §6.4.1 steps 4-12, given `k` already generated (step 3). +/// Returns `None` for step 11's `r = 0` or `s = 0` case; each caller acts on it as step 11 +/// directs for its way of generating `k`. +fn sign_with_k( + sk: &ECDSAP521PrivateKey, + e: &P521ScalarField, + k: P521Scalar, +) -> Option<[u8; SIG_LEN]> { + let mut k_field = P521ScalarField::from_secret(&k); + let mut k_inv = k_field.invert(); // step 4 + + let r_point = comb_multiply_base_point(&k); // step 5 + let x_r = x_affine_of_signing_point(&r_point); // steps 6-7 + let r = P521ScalarField::from_limbs(x_r.to_limbs()); // step 8 + + let mut d_field = P521ScalarField::from_secret(sk.scalar()); + let s = k_inv.mul(&e.add(&r.mul(&d_field))); // step 9 + + // `k_field`, `k_inv` and `d_field` hold `k`, `k^-1` and `d`. Nothing below needs them, so they + // are overwritten here rather than left legible on the stack after this returns -- placed + // before step 11's check so that both ways out of the function are covered. `k` itself is a + // `Secret` and scrubs when it drops; `e`, `r` and `s` are public by construction. + k_field.zeroize(); + k_inv.zeroize(); + d_field.zeroize(); + + if r_or_s_is_zero(&r, &s) { + return None; + } + + let mut sig = [0u8; SIG_LEN]; + sig[..66].copy_from_slice(&p521_sec1::be_bytes_from_limbs(&r.to_limbs())); + sig[66..].copy_from_slice(&p521_sec1::be_bytes_from_limbs(&s.to_limbs())); + Some(sig) +} + +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1, reduced into `[1, n-1]` by +/// `reduce_wide_bits_mod_n_minus_1`; the one place [`ECDSAP521`]'s signing paths draw `k`. Draws +/// A.3.1 step 3's `N + t` bits (`t = 71`, the byte-aligned width at or above `t = 64`) and reduces +/// them with [`bouncycastle_ec::p521_extra_bits`] -- see that module's docs. +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + +/// `e` from a 64-byte (SHA-512) message hash: `E = H` (`len(n) = 521 >= hashlen = 512`, so no +/// truncation -- see the module docs), zero-extended into a 9-limb value, then Appendix B.2.1's +/// big-endian conversion. +fn e_from_hash(h: &[u8; 64]) -> P521ScalarField { + let mut padded = [0u8; 66]; + padded[2..].copy_from_slice(h); + P521ScalarField::from_limbs(p521_sec1::limbs_from_be_bytes(&padded)) +} + +/// FIPS 186-5 §6.4.1 step 11's `r = 0` or `s = 0` check. See +/// [`crate::ecdsa_p256::r_or_s_is_zero`]'s docs. +fn r_or_s_is_zero(r: &P521ScalarField, s: &P521ScalarField) -> bool { + *r == P521ScalarField::ZERO || *s == P521ScalarField::ZERO +} + +/// FIPS 186-5 §6.4.2 on the finished message hash; the verifier's `do_verify_final` is +/// this one function. See `crate::ecdsa_p256`'s `verify_final`. +fn verify_final(hash: SHA512, pk: &ECDSAP521PublicKey, sig: &[u8]) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::LengthError("ECDSA P-521 signature must be exactly 132 bytes")); + } + + let r_limbs = p521_sec1::limbs_from_be_bytes(&sig[..66].try_into().unwrap()); + let s_limbs = p521_sec1::limbs_from_be_bytes(&sig[66..132].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = P521ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 64]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = P521PublicScalar::from_limbs(s_limbs).invert_vartime(); + let u = e.mul(&s_inv); + let v = r.mul(&s_inv); + + let q = P521JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = P521PublicScalar::from_limbs(u.to_limbs()); + let v_pub = P521PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = P521ScalarField::from_limbs(x_r1.to_limbs()); + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; see +/// `crate::ecdsa_p256`'s `der_encode`. +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..66], &raw[66..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(66); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + +impl ECDSAP521PrivateKey { + /// [`SigningKey::sign`] with the signature DER-encoded as RFC 3279 §2.2.3's + /// `SEQUENCE { r, s }` (see [`crate::der`]); returns the buffer and the encoded length. + pub fn sign_der( + &self, + rng: &mut dyn RNG, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign(rng, msg, ctx)?)) + } + + /// [`DeterministicSigningKey::sign_deterministic`], DER-encoded as [`Self::sign_der`]. + pub fn sign_der_deterministic( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&self.sign_deterministic(msg, ctx)?)) + } +} + +impl ECDSAP521PublicKey { + /// [`VerifyingKey::verify`] of a DER-encoded signature (RFC 3279 §2.2.3); a malformed + /// encoding is a verification failure. + pub fn verify_der( + &self, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + self.verify(msg, ctx, &der_decode(sig)?) + } +} + +impl SigningKey for ECDSAP521PrivateKey { + type Signer<'k> + = ECDSAP521Signer<'k> + where + Self: 'k; + + fn create_signer<'k>( + &'k self, + rng: &mut dyn RNG, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSAP521Signer { sk: self, hash: SHA512::default(), k: random_k(rng)? }) + } +} + +impl DeterministicSigningKey for ECDSAP521PrivateKey { + type Signer<'k> + = ECDetDSAP521Signer<'k> + where + Self: 'k; + + fn create_signer_deterministic<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDetDSAP521Signer { sk: self, hash: SHA512::default() }) + } +} + +impl VerifyingKey for ECDSAP521PublicKey { + type Verifier<'k> + = ECDSAP521Verifier<'k> + where + Self: 'k; + + fn create_verifier<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(ECDSAP521Verifier { pk: self, hash: SHA512::default() }) + } +} + +impl AlgorithmInfo for ECDSAP521Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDSAP521::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSAP521::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDSAP521Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 64]; + self.hash.do_final_out(&mut h); + // FIPS 186-5 §6.4.1 step 11: with a random `k`, `r = 0` or `s = 0` goes back to step 3. + // `k` was drawn when this signer was created and there is no RNG here to draw another + // from, so the failure is reported instead; it has probability about 2^-520. + sign_with_k(self.sk, &e_from_hash(&h), self.k).ok_or(SignatureError::GenericError( + "ECDSA produced r = 0 or s = 0; create a fresh signer and sign again", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDetDSAP521Signer<'_> { + fn alg_name(&self) -> &'static str { + ECDETDSA_P521_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSAP521::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for ECDetDSAP521Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let mut h = [0u8; 64]; + self.hash.do_final_out(&mut h); + let k = rfc6979_p521::generate_k(self.sk.scalar(), &h); // step 3, Appendix A.3.3 + // FIPS 186-5 §6.4.1 step 11: with a deterministic `k`, `r = 0` or `s = 0` outputs failure. + sign_with_k(self.sk, &e_from_hash(&h), k).ok_or(SignatureError::GenericError( + "deterministic ECDSA produced r = 0 or s = 0; FIPS 186-5 §6.4.1 step 11: output failure", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for ECDSAP521Verifier<'_> { + fn alg_name(&self) -> &'static str { + ECDSAP521::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + ECDSAP521::MAX_SECURITY_STRENGTH + } +} + +impl SignatureVerifier for ECDSAP521Verifier<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + verify_final(self.hash, self.pk, sig) + } +} + +fn in_range(limbs: &[u64; 9]) -> bool { + if *limbs == [0; 9] { + return false; + } + let (_, borrow) = nat::sub(limbs, &N_LIMBS); + borrow == 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn r_or_s_is_zero_requires_only_one_to_be_zero() { + let zero = P521ScalarField::ZERO; + let one = P521ScalarField::ONE; + assert!(!r_or_s_is_zero(&one, &one)); + assert!(r_or_s_is_zero(&zero, &one)); + assert!(r_or_s_is_zero(&one, &zero)); + assert!(r_or_s_is_zero(&zero, &zero)); + } +} diff --git a/crypto/ecdsa/src/keys_bp256r1.rs b/crypto/ecdsa/src/keys_bp256r1.rs new file mode 100644 index 000000000..4aeb882da --- /dev/null +++ b/crypto/ecdsa/src/keys_bp256r1.rs @@ -0,0 +1,155 @@ +//! ECDSA brainpoolP256r1 key types (FIPS 186-5 §6.2) and key-pair generation (Appendix A.2.1). +//! Identical in shape to [`crate::keys_p256`] -- see that module's docs for the full reasoning -- with +//! brainpoolP256r1's types and SEC 1 encoding widths substituted, and [`bouncycastle_ec::bp256r1_extra_bits`] +//! in place of [`bouncycastle_ec::p256_extra_bits`] for the wide-DRBG-output reduction this curve's `n` needs (see +//! that module's docs for why). + +use crate::keys_common::DerivePublicKey; +use bouncycastle_core::errors::{RNGError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::bp256r1::Bp256r1FieldElement; +use bouncycastle_ec::bp256r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp256r1_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::bp256r1_scalar::{Bp256r1Scalar, N_LIMBS}; +use bouncycastle_ec::bp256r1_sec1; +use bouncycastle_ec::nat; +use bouncycastle_utils::secret::Secret; +use core::fmt; +use core::fmt::{Debug, Display, Formatter}; + +/// Encoded length of a brainpoolP256r1 private key: SEC 1 §2.3.7 integer-to-octet-string of `d`, +/// 32 bytes. +pub const SK_LEN: usize = 32; + +/// Encoded length of a brainpoolP256r1 public key in the canonical uncompressed form (SEC 1 +/// §2.3.3, `04 || X || Y`); [`ECDSABp256r1PublicKey::from_bytes`] also accepts the 33-byte +/// compressed form. +pub const PK_LEN: usize = 65; + +/// Requested output length, in bytes, from the DRBG for FIPS 186-5 Appendix A.2.1 key generation +/// and Appendix A.3.1 randomised per-message secret generation: `320` bits (40 bytes) -- derived +/// directly from Appendix A.4.1's own bias-bound check for brainpoolP256r1's `n` (see +/// [`bouncycastle_ec::bp256r1_extra_bits`]'s docs), not copied from any other curve's table entry. +pub(crate) const EXTRA_BITS_DRBG_OUTPUT_LEN: usize = 40; + +/// An ECDSA brainpoolP256r1 private key: FIPS 186-5 §6.2's `d`, `d` in `[1, n-1]`, held in +/// [`bouncycastle_utils::secret::Secret`] via [`Bp256r1Scalar`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ECDSABp256r1PrivateKey(Bp256r1Scalar); + +impl ECDSABp256r1PrivateKey { + /// The wrapped scalar, for this crate's own sign implementation to compute with. + pub(crate) fn scalar(&self) -> &Bp256r1Scalar { + &self.0 + } +} + +impl DerivePublicKey for ECDSABp256r1PrivateKey { + fn derive_pk(&self) -> ECDSABp256r1PublicKey { + let q = comb_multiply_base_point(&self.0); + // d is in [1, n-1] by construction (every ECDSABp256r1PrivateKey is built that way; see + // from_bytes and keygen) and G has prime order n, so [d]G is never the identity. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + ECDSABp256r1PublicKey { x, y } + } +} + +impl SignaturePrivateKey for ECDSABp256r1PrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.0.to_be_bytes() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + out.fill(0); + *out = self.0.to_be_bytes(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let array: [u8; SK_LEN] = bytes.try_into().map_err(|_| { + SignatureError::DecodingError("ECDSA brainpoolP256r1 private key must be 32 bytes") + })?; + // FIPS 186-5 §6.2: d is in [1, n-1], checked on the raw big-endian value *before* any + // reduction. `Bp256r1Scalar::from_be_bytes` reduces mod n, so checking afterwards would + // accept an out-of-range encoding as a different, perfectly valid key: `d = n + 1` would + // load as `d = 1`, giving one key two encodings and silently treating malformed input as + // well-formed. Comparing against the fixed public values 0 and n is a one-time validation + // of caller-supplied bytes at load time, not a computation performed repeatedly on a secret + // intermediate value, so branching on it leaks nothing beyond what the caller already knows + // from having supplied these exact bytes. + let limbs = bp256r1_sec1::limbs_from_be_bytes(&array); + let (_, borrow) = nat::sub(&limbs, &N_LIMBS); + if limbs == [0; 4] || borrow != 1 { + return Err(SignatureError::DecodingError( + "ECDSA brainpoolP256r1 private key must be in [1, n-1]", + )); + } + Ok(Self(Bp256r1Scalar::from_limbs(limbs))) + } +} + +/// An ECDSA brainpoolP256r1 public key: FIPS 186-5 §6.2's `Q = [d]G`, stored as affine +/// coordinates. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ECDSABp256r1PublicKey { + pub(crate) x: Bp256r1FieldElement, + pub(crate) y: Bp256r1FieldElement, +} + +impl SignaturePublicKey for ECDSABp256r1PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + bp256r1_sec1::encode_uncompressed(&self.x, &self.y) + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + out.fill(0); + *out = self.encode(); + PK_LEN + } + + /// Accepts both SEC 1 §2.3.3 encodings (33-byte compressed, 65-byte uncompressed), fully + /// validated per [`bp256r1_sec1::decode`]'s docs (partial validation is full validation here: + /// RFC 5639 §3.4 gives brainpoolP256r1 cofactor `h = 1`). + fn from_bytes(bytes: &[u8]) -> Result { + let (x, y) = bp256r1_sec1::decode(bytes).ok_or(SignatureError::DecodingError( + "invalid or out-of-range SEC 1 brainpoolP256r1 point encoding", + ))?; + Ok(Self { x, y }) + } +} + +impl Display for ECDSABp256r1PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "ECDSABp256r1PublicKey {{ {:x?} }}", self.encode()) + } +} + +/// As [`keygen`], but sources the DRBG output from the caller-provided RNG, which must +/// offer a security strength of at least 128 bits: FIPS 186-5 Appendix A.2.1 step 3 requires +/// a DRBG whose security strength is "not less than" the one SP 800-57 Part 1 Rev. 5's +/// Table 2 associates with brainpoolP256r1's 256-bit order (`f = 256-383` in that table's ECC column, giving +/// 128-bit security). +pub fn keygen( + rng: &mut dyn RNG, +) -> Result<(ECDSABp256r1PublicKey, ECDSABp256r1PrivateKey), SignatureError> { + // FIPS 186-5 Appendix A.2.1 step 3: the DRBG must offer at least the security + // strength brainpoolP256r1's 256-bit order calls for (SP 800-57 Part 1 Rev. 5, Table 2: 128 bits). + if rng.security_strength() < SecurityStrength::_128bit { + return Err(SignatureError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)); + } + + // Raw DRBG output, reduced below into the private key / per-message secret: held in + // `Secret` so it is scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + let d = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + + let q = comb_multiply_base_point(&d); + // d is in [1, n-1] by construction (see reduce_wide_bits_mod_n_minus_1) and G has prime order + // n (RFC 5639 §3.4's cofactor h = 1), so [d]G is never the identity: this holds for every + // valid d, so branching on it here leaks nothing about which d was drawn. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + + Ok((ECDSABp256r1PublicKey { x, y }, ECDSABp256r1PrivateKey(d))) +} diff --git a/crypto/ecdsa/src/keys_bp384r1.rs b/crypto/ecdsa/src/keys_bp384r1.rs new file mode 100644 index 000000000..0eb291d50 --- /dev/null +++ b/crypto/ecdsa/src/keys_bp384r1.rs @@ -0,0 +1,155 @@ +//! ECDSA brainpoolP384r1 key types (FIPS 186-5 §6.2) and key-pair generation (Appendix A.2.1). +//! Identical in shape to [`crate::keys_p256`] -- see that module's docs for the full reasoning -- with +//! brainpoolP384r1's types and SEC 1 encoding widths substituted, and [`bouncycastle_ec::bp384r1_extra_bits`] +//! in place of [`bouncycastle_ec::p256_extra_bits`] for the wide-DRBG-output reduction this curve's `n` needs (see +//! that module's docs for why). + +use crate::keys_common::DerivePublicKey; +use bouncycastle_core::errors::{RNGError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::bp384r1::Bp384r1FieldElement; +use bouncycastle_ec::bp384r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp384r1_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::bp384r1_scalar::{Bp384r1Scalar, N_LIMBS}; +use bouncycastle_ec::bp384r1_sec1; +use bouncycastle_ec::nat; +use bouncycastle_utils::secret::Secret; +use core::fmt; +use core::fmt::{Debug, Display, Formatter}; + +/// Encoded length of a brainpoolP384r1 private key: SEC 1 §2.3.7 integer-to-octet-string of `d`, +/// 48 bytes. +pub const SK_LEN: usize = 48; + +/// Encoded length of a brainpoolP384r1 public key in the canonical uncompressed form (SEC 1 +/// §2.3.3, `04 || X || Y`); [`ECDSABp384r1PublicKey::from_bytes`] also accepts the 49-byte +/// compressed form. +pub const PK_LEN: usize = 97; + +/// Requested output length, in bytes, from the DRBG for FIPS 186-5 Appendix A.2.1 key generation +/// and Appendix A.3.1 randomised per-message secret generation: `448` bits (56 bytes) -- derived +/// directly from Appendix A.4.1's own bias-bound check for brainpoolP384r1's `n` (see +/// [`bouncycastle_ec::bp384r1_extra_bits`]'s docs), not copied from any other curve's table entry. +pub(crate) const EXTRA_BITS_DRBG_OUTPUT_LEN: usize = 56; + +/// An ECDSA brainpoolP384r1 private key: FIPS 186-5 §6.2's `d`, `d` in `[1, n-1]`, held in +/// [`bouncycastle_utils::secret::Secret`] via [`Bp384r1Scalar`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ECDSABp384r1PrivateKey(Bp384r1Scalar); + +impl ECDSABp384r1PrivateKey { + /// The wrapped scalar, for this crate's own sign implementation to compute with. + pub(crate) fn scalar(&self) -> &Bp384r1Scalar { + &self.0 + } +} + +impl DerivePublicKey for ECDSABp384r1PrivateKey { + fn derive_pk(&self) -> ECDSABp384r1PublicKey { + let q = comb_multiply_base_point(&self.0); + // d is in [1, n-1] by construction (every ECDSABp384r1PrivateKey is built that way; see + // from_bytes and keygen) and G has prime order n, so [d]G is never the identity. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + ECDSABp384r1PublicKey { x, y } + } +} + +impl SignaturePrivateKey for ECDSABp384r1PrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.0.to_be_bytes() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + out.fill(0); + *out = self.0.to_be_bytes(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let array: [u8; SK_LEN] = bytes.try_into().map_err(|_| { + SignatureError::DecodingError("ECDSA brainpoolP384r1 private key must be 48 bytes") + })?; + // FIPS 186-5 §6.2: d is in [1, n-1], checked on the raw big-endian value *before* any + // reduction. `Bp384r1Scalar::from_be_bytes` reduces mod n, so checking afterwards would + // accept an out-of-range encoding as a different, perfectly valid key: `d = n + 1` would + // load as `d = 1`, giving one key two encodings and silently treating malformed input as + // well-formed. Comparing against the fixed public values 0 and n is a one-time validation + // of caller-supplied bytes at load time, not a computation performed repeatedly on a secret + // intermediate value, so branching on it leaks nothing beyond what the caller already knows + // from having supplied these exact bytes. + let limbs = bp384r1_sec1::limbs_from_be_bytes(&array); + let (_, borrow) = nat::sub(&limbs, &N_LIMBS); + if limbs == [0; 6] || borrow != 1 { + return Err(SignatureError::DecodingError( + "ECDSA brainpoolP384r1 private key must be in [1, n-1]", + )); + } + Ok(Self(Bp384r1Scalar::from_limbs(limbs))) + } +} + +/// An ECDSA brainpoolP384r1 public key: FIPS 186-5 §6.2's `Q = [d]G`, stored as affine +/// coordinates. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ECDSABp384r1PublicKey { + pub(crate) x: Bp384r1FieldElement, + pub(crate) y: Bp384r1FieldElement, +} + +impl SignaturePublicKey for ECDSABp384r1PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + bp384r1_sec1::encode_uncompressed(&self.x, &self.y) + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + out.fill(0); + *out = self.encode(); + PK_LEN + } + + /// Accepts both SEC 1 §2.3.3 encodings (49-byte compressed, 97-byte uncompressed), fully + /// validated per [`bp384r1_sec1::decode`]'s docs (partial validation is full validation here: + /// RFC 5639 §3.4 gives brainpoolP384r1 cofactor `h = 1`). + fn from_bytes(bytes: &[u8]) -> Result { + let (x, y) = bp384r1_sec1::decode(bytes).ok_or(SignatureError::DecodingError( + "invalid or out-of-range SEC 1 brainpoolP384r1 point encoding", + ))?; + Ok(Self { x, y }) + } +} + +impl Display for ECDSABp384r1PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "ECDSABp384r1PublicKey {{ {:x?} }}", self.encode()) + } +} + +/// As [`keygen`], but sources the DRBG output from the caller-provided RNG, which must +/// offer a security strength of at least 192 bits: FIPS 186-5 Appendix A.2.1 step 3 requires +/// a DRBG whose security strength is "not less than" the one SP 800-57 Part 1 Rev. 5's +/// Table 2 associates with brainpoolP384r1's 384-bit order (`f = 384-511` in that table's ECC column, giving +/// 192-bit security). +pub fn keygen( + rng: &mut dyn RNG, +) -> Result<(ECDSABp384r1PublicKey, ECDSABp384r1PrivateKey), SignatureError> { + // FIPS 186-5 Appendix A.2.1 step 3: the DRBG must offer at least the security + // strength brainpoolP384r1's 384-bit order calls for (SP 800-57 Part 1 Rev. 5, Table 2: 192 bits). + if rng.security_strength() < SecurityStrength::_192bit { + return Err(SignatureError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)); + } + + // Raw DRBG output, reduced below into the private key / per-message secret: held in + // `Secret` so it is scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + let d = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + + let q = comb_multiply_base_point(&d); + // d is in [1, n-1] by construction (see reduce_wide_bits_mod_n_minus_1) and G has prime order + // n (RFC 5639 §3.4's cofactor h = 1), so [d]G is never the identity: this holds for every + // valid d, so branching on it here leaks nothing about which d was drawn. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + + Ok((ECDSABp384r1PublicKey { x, y }, ECDSABp384r1PrivateKey(d))) +} diff --git a/crypto/ecdsa/src/keys_bp512r1.rs b/crypto/ecdsa/src/keys_bp512r1.rs new file mode 100644 index 000000000..e18b38e3b --- /dev/null +++ b/crypto/ecdsa/src/keys_bp512r1.rs @@ -0,0 +1,155 @@ +//! ECDSA brainpoolP512r1 key types (FIPS 186-5 §6.2) and key-pair generation (Appendix A.2.1). +//! Identical in shape to [`crate::keys_p256`] -- see that module's docs for the full reasoning -- with +//! brainpoolP512r1's types and SEC 1 encoding widths substituted, and [`bouncycastle_ec::bp512r1_extra_bits`] +//! in place of [`bouncycastle_ec::p256_extra_bits`] for the wide-DRBG-output reduction this curve's `n` needs (see +//! that module's docs for why). + +use crate::keys_common::DerivePublicKey; +use bouncycastle_core::errors::{RNGError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::bp512r1::Bp512r1FieldElement; +use bouncycastle_ec::bp512r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp512r1_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::bp512r1_scalar::{Bp512r1Scalar, N_LIMBS}; +use bouncycastle_ec::bp512r1_sec1; +use bouncycastle_ec::nat; +use bouncycastle_utils::secret::Secret; +use core::fmt; +use core::fmt::{Debug, Display, Formatter}; + +/// Encoded length of a brainpoolP512r1 private key: SEC 1 §2.3.7 integer-to-octet-string of `d`, +/// 64 bytes. +pub const SK_LEN: usize = 64; + +/// Encoded length of a brainpoolP512r1 public key in the canonical uncompressed form (SEC 1 +/// §2.3.3, `04 || X || Y`); [`ECDSABp512r1PublicKey::from_bytes`] also accepts the 65-byte +/// compressed form. +pub const PK_LEN: usize = 129; + +/// Requested output length, in bytes, from the DRBG for FIPS 186-5 Appendix A.2.1 key generation +/// and Appendix A.3.1 randomised per-message secret generation: `576` bits (72 bytes) -- derived +/// directly from Appendix A.4.1's own bias-bound check for brainpoolP512r1's `n` (see +/// [`bouncycastle_ec::bp512r1_extra_bits`]'s docs), not copied from any other curve's table entry. +pub(crate) const EXTRA_BITS_DRBG_OUTPUT_LEN: usize = 72; + +/// An ECDSA brainpoolP512r1 private key: FIPS 186-5 §6.2's `d`, `d` in `[1, n-1]`, held in +/// [`bouncycastle_utils::secret::Secret`] via [`Bp512r1Scalar`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ECDSABp512r1PrivateKey(Bp512r1Scalar); + +impl ECDSABp512r1PrivateKey { + /// The wrapped scalar, for this crate's own sign implementation to compute with. + pub(crate) fn scalar(&self) -> &Bp512r1Scalar { + &self.0 + } +} + +impl DerivePublicKey for ECDSABp512r1PrivateKey { + fn derive_pk(&self) -> ECDSABp512r1PublicKey { + let q = comb_multiply_base_point(&self.0); + // d is in [1, n-1] by construction (every ECDSABp512r1PrivateKey is built that way; see + // from_bytes and keygen) and G has prime order n, so [d]G is never the identity. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + ECDSABp512r1PublicKey { x, y } + } +} + +impl SignaturePrivateKey for ECDSABp512r1PrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.0.to_be_bytes() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + out.fill(0); + *out = self.0.to_be_bytes(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let array: [u8; SK_LEN] = bytes.try_into().map_err(|_| { + SignatureError::DecodingError("ECDSA brainpoolP512r1 private key must be 64 bytes") + })?; + // FIPS 186-5 §6.2: d is in [1, n-1], checked on the raw big-endian value *before* any + // reduction. `Bp512r1Scalar::from_be_bytes` reduces mod n, so checking afterwards would + // accept an out-of-range encoding as a different, perfectly valid key: `d = n + 1` would + // load as `d = 1`, giving one key two encodings and silently treating malformed input as + // well-formed. Comparing against the fixed public values 0 and n is a one-time validation + // of caller-supplied bytes at load time, not a computation performed repeatedly on a secret + // intermediate value, so branching on it leaks nothing beyond what the caller already knows + // from having supplied these exact bytes. + let limbs = bp512r1_sec1::limbs_from_be_bytes(&array); + let (_, borrow) = nat::sub(&limbs, &N_LIMBS); + if limbs == [0; 8] || borrow != 1 { + return Err(SignatureError::DecodingError( + "ECDSA brainpoolP512r1 private key must be in [1, n-1]", + )); + } + Ok(Self(Bp512r1Scalar::from_limbs(limbs))) + } +} + +/// An ECDSA brainpoolP512r1 public key: FIPS 186-5 §6.2's `Q = [d]G`, stored as affine +/// coordinates. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ECDSABp512r1PublicKey { + pub(crate) x: Bp512r1FieldElement, + pub(crate) y: Bp512r1FieldElement, +} + +impl SignaturePublicKey for ECDSABp512r1PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + bp512r1_sec1::encode_uncompressed(&self.x, &self.y) + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + out.fill(0); + *out = self.encode(); + PK_LEN + } + + /// Accepts both SEC 1 §2.3.3 encodings (65-byte compressed, 129-byte uncompressed), fully + /// validated per [`bp512r1_sec1::decode`]'s docs (partial validation is full validation here: + /// RFC 5639 §3.4 gives brainpoolP512r1 cofactor `h = 1`). + fn from_bytes(bytes: &[u8]) -> Result { + let (x, y) = bp512r1_sec1::decode(bytes).ok_or(SignatureError::DecodingError( + "invalid or out-of-range SEC 1 brainpoolP512r1 point encoding", + ))?; + Ok(Self { x, y }) + } +} + +impl Display for ECDSABp512r1PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "ECDSABp512r1PublicKey {{ {:x?} }}", self.encode()) + } +} + +/// As [`keygen`], but sources the DRBG output from the caller-provided RNG, which must +/// offer a security strength of at least 256 bits: FIPS 186-5 Appendix A.2.1 step 3 requires +/// a DRBG whose security strength is "not less than" the one SP 800-57 Part 1 Rev. 5's +/// Table 2 associates with brainpoolP512r1's 512-bit order (`f = 512+` in that table's ECC column, giving +/// 256-bit security). +pub fn keygen( + rng: &mut dyn RNG, +) -> Result<(ECDSABp512r1PublicKey, ECDSABp512r1PrivateKey), SignatureError> { + // FIPS 186-5 Appendix A.2.1 step 3: the DRBG must offer at least the security + // strength brainpoolP512r1's 512-bit order calls for (SP 800-57 Part 1 Rev. 5, Table 2: 256 bits). + if rng.security_strength() < SecurityStrength::_256bit { + return Err(SignatureError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)); + } + + // Raw DRBG output, reduced below into the private key / per-message secret: held in + // `Secret` so it is scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + let d = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + + let q = comb_multiply_base_point(&d); + // d is in [1, n-1] by construction (see reduce_wide_bits_mod_n_minus_1) and G has prime order + // n (RFC 5639 §3.4's cofactor h = 1), so [d]G is never the identity: this holds for every + // valid d, so branching on it here leaks nothing about which d was drawn. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + + Ok((ECDSABp512r1PublicKey { x, y }, ECDSABp512r1PrivateKey(d))) +} diff --git a/crypto/ecdsa/src/keys_common.rs b/crypto/ecdsa/src/keys_common.rs new file mode 100644 index 000000000..e8e2f1c36 --- /dev/null +++ b/crypto/ecdsa/src/keys_common.rs @@ -0,0 +1,19 @@ +//! [`DerivePublicKey`]: a small trait every curve's private key type in this crate implements, so +//! `d`'s matching public key `Q = [d]G` can be recomputed on demand rather than only ever obtained +//! from `keys::keygen` (or its per-curve equivalents) at generation time. Exists for the CLI's +//! `PkFromSk`/`CheckConsistency` actions, mirroring the shape (if not the name) of +//! `bouncycastle_mldsa::MLDSAPrivateKeyTrait::derive_pk`/`bouncycastle_mlkem::MLKEMPrivateKeyTrait::pk`, +//! which those crates' own CLI commands already use for the same purpose -- kept local to this +//! crate rather than added to `bouncycastle_core::traits`, since no other primitive in this +//! workspace shares this exact shape (a public key that is *always* a fixed, cheap function of the +//! private key alone, with no additional state). + +use bouncycastle_core::traits::SignaturePublicKey; + +/// Implemented by a curve's private key type: recomputes the matching public key `Q = [d]G` +/// directly from the wrapped private scalar `d`, using the same fixed-base multiplier +/// `keys_p256::keygen` (or its per-curve equivalent) uses internally. +pub trait DerivePublicKey, const PK_LEN: usize> { + /// Recomputes `Q = [d]G`. + fn derive_pk(&self) -> PK; +} diff --git a/crypto/ecdsa/src/keys_p256.rs b/crypto/ecdsa/src/keys_p256.rs new file mode 100644 index 000000000..97592162d --- /dev/null +++ b/crypto/ecdsa/src/keys_p256.rs @@ -0,0 +1,149 @@ +//! ECDSA P-256 key types (FIPS 186-5 §6.2) and key-pair generation (Appendix A.2.1). + +use crate::keys_common::DerivePublicKey; +use bouncycastle_core::errors::{RNGError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::nat; +use bouncycastle_ec::p256::P256FieldElement; +use bouncycastle_ec::p256_comb::comb_multiply_base_point; +use bouncycastle_ec::p256_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::p256_scalar::{N_LIMBS, P256Scalar}; +use bouncycastle_ec::p256_sec1; +use bouncycastle_utils::secret::Secret; +use core::fmt; +use core::fmt::{Debug, Display, Formatter}; + +/// Encoded length of a P-256 private key: SEC 1 §2.3.7 integer-to-octet-string of `d`, `ceil(log2 +/// n / 8)` = 32 bytes. +pub const SK_LEN: usize = 32; + +/// Encoded length of a P-256 public key in the canonical form [`ECDSAP256PublicKey::encode`] +/// emits: SEC 1 §2.3.3's uncompressed `04 || X || Y`, 65 bytes. +/// [`ECDSAP256PublicKey::from_bytes`] also accepts the 33-byte compressed form. +pub const PK_LEN: usize = 65; + +/// Requested output length, in bytes, from the DRBG for FIPS 186-5 Appendix A.2.1 key generation +/// and Appendix A.3.1 randomised per-message secret generation: FIPS 186-5 Table A.2 (in +/// Appendix A.2.1) gives 352 bits in its "Recommended" column for `p256`, i.e. `N + t` with `N = +/// 256` and `t = 96 >= 64`. +pub(crate) const EXTRA_BITS_DRBG_OUTPUT_LEN: usize = 44; + +/// An ECDSA P-256 private key: FIPS 186-5 §6.2's `d`, `d` in `[1, n-1]`, held in +/// [`bouncycastle_utils::secret::Secret`] via [`P256Scalar`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ECDSAP256PrivateKey(P256Scalar); + +impl ECDSAP256PrivateKey { + /// The wrapped scalar, for this crate's own sign implementation to compute with. + pub(crate) fn scalar(&self) -> &P256Scalar { + &self.0 + } +} + +impl DerivePublicKey for ECDSAP256PrivateKey { + fn derive_pk(&self) -> ECDSAP256PublicKey { + let q = comb_multiply_base_point(&self.0); + // d is in [1, n-1] by construction (every ECDSAP256PrivateKey is built that way; see + // from_bytes and keygen) and G has prime order n, so [d]G is never the identity. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + ECDSAP256PublicKey { x, y } + } +} + +impl SignaturePrivateKey for ECDSAP256PrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.0.to_be_bytes() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + out.fill(0); + *out = self.0.to_be_bytes(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let array: [u8; SK_LEN] = bytes.try_into().map_err(|_| { + SignatureError::DecodingError("ECDSA P-256 private key must be 32 bytes") + })?; + // FIPS 186-5 §6.2: d is in [1, n-1], checked on the raw big-endian value *before* any + // reduction. `P256Scalar::from_be_bytes` reduces mod n, so checking afterwards would accept + // an out-of-range encoding as a different, perfectly valid key: `d = n + 1` would load as + // `d = 1`, giving one key two encodings and silently treating malformed input as well- + // formed. Comparing against the fixed public values 0 and n is a one-time validation of + // caller-supplied bytes at load time, not a computation performed repeatedly on a secret + // intermediate value, so branching on it leaks nothing beyond what the caller already knows + // from having supplied these exact bytes. + let limbs = p256_sec1::limbs_from_be_bytes(&array); + let (_, borrow) = nat::sub(&limbs, &N_LIMBS); + if limbs == [0; 4] || borrow != 1 { + return Err(SignatureError::DecodingError( + "ECDSA P-256 private key must be in [1, n-1]", + )); + } + Ok(Self(P256Scalar::from_limbs(limbs))) + } +} + +/// An ECDSA P-256 public key: FIPS 186-5 §6.2's `Q = [d]G`, stored as affine coordinates. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ECDSAP256PublicKey { + pub(crate) x: P256FieldElement, + pub(crate) y: P256FieldElement, +} + +impl SignaturePublicKey for ECDSAP256PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + p256_sec1::encode_uncompressed(&self.x, &self.y) + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + out.fill(0); + *out = self.encode(); + PK_LEN + } + + /// Accepts both SEC 1 §2.3.3 encodings (33-byte compressed, 65-byte uncompressed), fully + /// validated per SP 800-186 Appendix D.1.1.1 (see [`p256_sec1::decode`]'s docs). + fn from_bytes(bytes: &[u8]) -> Result { + let (x, y) = p256_sec1::decode(bytes).ok_or(SignatureError::DecodingError( + "invalid or out-of-range SEC 1 P-256 point encoding", + ))?; + Ok(Self { x, y }) + } +} + +impl Display for ECDSAP256PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "ECDSAP256PublicKey {{ {:x?} }}", self.encode()) + } +} + +/// As [`keygen`], but sources the DRBG output from the caller-provided RNG, which must +/// offer a security strength of at least 128 bits: FIPS 186-5 Appendix A.2.1 step 3 requires +/// a DRBG whose security strength is "not less than" the one SP 800-57 Part 1 Rev. 5's +/// Table 2 associates with P-256's 256-bit order (`f = 256-383` in that table's ECC column, giving +/// 128-bit security). +pub fn keygen( + rng: &mut dyn RNG, +) -> Result<(ECDSAP256PublicKey, ECDSAP256PrivateKey), SignatureError> { + // FIPS 186-5 Appendix A.2.1 step 3: the DRBG must offer at least the security + // strength P-256's 256-bit order calls for (SP 800-57 Part 1 Rev. 5, Table 2: 128 bits). + if rng.security_strength() < SecurityStrength::_128bit { + return Err(SignatureError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)); + } + + // Raw DRBG output, reduced below into the private key / per-message secret: held in + // `Secret` so it is scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + let d = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + + let q = comb_multiply_base_point(&d); + // d is in [1, n-1] by construction (see reduce_wide_bits_mod_n_minus_1) and G has prime order + // n (SP 800-186 §3.2.1.3's cofactor h = 1), so [d]G is never the identity: this holds for every + // valid d, so branching on it here leaks nothing about which d was drawn. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + + Ok((ECDSAP256PublicKey { x, y }, ECDSAP256PrivateKey(d))) +} diff --git a/crypto/ecdsa/src/keys_p256k1.rs b/crypto/ecdsa/src/keys_p256k1.rs new file mode 100644 index 000000000..456c45823 --- /dev/null +++ b/crypto/ecdsa/src/keys_p256k1.rs @@ -0,0 +1,154 @@ +//! ECDSA secp256k1 key types (FIPS 186-5 §6.2) and key-pair generation (Appendix A.2.1). Identical +//! in shape to [`crate::keys_p256`] -- see that module's docs for the full reasoning -- with +//! secp256k1's types and SEC 1 encoding widths substituted. +//! +//! # DRBG output length +//! +//! secp256k1's `n` is within `2^129` of `2^256`, so Appendix A.4.1's bias bound already holds at +//! 256 bits and key generation would need no headroom. But Appendix A.3.1 -- the randomised +//! per-message secret -- requires `N + t` bits with `t >= 64` regardless, so both draws are 320 +//! bits (40 bytes) and go through [`bouncycastle_ec::p256k1_extra_bits`]'s wide reduction; see that +//! module's docs for the reasoning and for why key generation uses the same draw. + +use crate::keys_common::DerivePublicKey; +use bouncycastle_core::errors::{RNGError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::nat; +use bouncycastle_ec::p256k1::P256K1FieldElement; +use bouncycastle_ec::p256k1_comb::comb_multiply_base_point; +use bouncycastle_ec::p256k1_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::p256k1_scalar::{N_LIMBS, P256K1Scalar}; +use bouncycastle_ec::p256k1_sec1; +use bouncycastle_utils::secret::Secret; +use core::fmt; +use core::fmt::{Debug, Display, Formatter}; + +/// Encoded length of a secp256k1 private key: SEC 1 §2.3.7 integer-to-octet-string of `d`, 32 +/// bytes. +pub const SK_LEN: usize = 32; + +/// Encoded length of a secp256k1 public key in the canonical uncompressed form (SEC 1 §2.3.3, +/// `04 || X || Y`); [`ECDSASecp256K1PublicKey::from_bytes`] also accepts the 33-byte compressed +/// form. +pub const PK_LEN: usize = 65; + +/// Requested output length, in bytes, from the DRBG for FIPS 186-5 Appendix A.2.1 key generation +/// and Appendix A.3.1 randomised per-message secret generation: `N + t` with `N = 256` and +/// `t = 64`, the minimum A.3.1 step 2 accepts, giving 320 bits. See the module docs. +pub(crate) const EXTRA_BITS_DRBG_OUTPUT_LEN: usize = 40; + +/// An ECDSA secp256k1 private key: FIPS 186-5 §6.2's `d`, `d` in `[1, n-1]`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ECDSASecp256K1PrivateKey(P256K1Scalar); + +impl ECDSASecp256K1PrivateKey { + /// The wrapped scalar, for this crate's own sign implementation to compute with. + pub(crate) fn scalar(&self) -> &P256K1Scalar { + &self.0 + } +} + +impl DerivePublicKey for ECDSASecp256K1PrivateKey { + fn derive_pk(&self) -> ECDSASecp256K1PublicKey { + let q = comb_multiply_base_point(&self.0); + // d is in [1, n-1] by construction (every ECDSASecp256K1PrivateKey is built that way; see + // from_bytes and keygen) and G has prime order n, so [d]G is never the identity. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + ECDSASecp256K1PublicKey { x, y } + } +} + +impl SignaturePrivateKey for ECDSASecp256K1PrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.0.to_be_bytes() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + out.fill(0); + *out = self.0.to_be_bytes(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let array: [u8; SK_LEN] = bytes.try_into().map_err(|_| { + SignatureError::DecodingError("ECDSA secp256k1 private key must be 32 bytes") + })?; + // FIPS 186-5 §6.2: d is in [1, n-1], checked on the raw big-endian value *before* any + // reduction. `P256K1Scalar::from_be_bytes` reduces mod n, so checking afterwards would + // accept an out-of-range encoding as a different, perfectly valid key: `d = n + 1` would + // load as `d = 1`, giving one key two encodings and silently treating malformed input as + // well-formed. Comparing against the fixed public values 0 and n is a one-time validation + // of caller-supplied bytes at load time, not a computation performed repeatedly on a secret + // intermediate value, so branching on it leaks nothing beyond what the caller already knows + // from having supplied these exact bytes. + let limbs = p256k1_sec1::limbs_from_be_bytes(&array); + let (_, borrow) = nat::sub(&limbs, &N_LIMBS); + if limbs == [0; 4] || borrow != 1 { + return Err(SignatureError::DecodingError( + "ECDSA secp256k1 private key must be in [1, n-1]", + )); + } + Ok(Self(P256K1Scalar::from_limbs(limbs))) + } +} + +/// An ECDSA secp256k1 public key: FIPS 186-5 §6.2's `Q = [d]G`, stored as affine coordinates. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ECDSASecp256K1PublicKey { + pub(crate) x: P256K1FieldElement, + pub(crate) y: P256K1FieldElement, +} + +impl SignaturePublicKey for ECDSASecp256K1PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + p256k1_sec1::encode_uncompressed(&self.x, &self.y) + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + out.fill(0); + *out = self.encode(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let (x, y) = p256k1_sec1::decode(bytes).ok_or(SignatureError::DecodingError( + "invalid or out-of-range SEC 1 secp256k1 point encoding", + ))?; + Ok(Self { x, y }) + } +} + +impl Display for ECDSASecp256K1PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "ECDSASecp256K1PublicKey {{ {:x?} }}", self.encode()) + } +} + +/// As [`keygen`], but sources the DRBG output from the caller-provided RNG, which must +/// offer a security strength of at least 128 bits: FIPS 186-5 Appendix A.2.1 step 3 requires +/// a DRBG whose security strength is "not less than" the one SP 800-57 Part 1 Rev. 5's +/// Table 2 associates with secp256k1's 256-bit order (`f = 256-383` in that table's ECC column, giving +/// 128-bit security). +pub fn keygen( + rng: &mut dyn RNG, +) -> Result<(ECDSASecp256K1PublicKey, ECDSASecp256K1PrivateKey), SignatureError> { + // FIPS 186-5 Appendix A.2.1 step 3: the DRBG must offer at least the security + // strength secp256k1's 256-bit order calls for (SP 800-57 Part 1 Rev. 5, Table 2: 128 bits). + if rng.security_strength() < SecurityStrength::_128bit { + return Err(SignatureError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)); + } + + // Raw DRBG output, reduced below into the private key / per-message secret: held in + // `Secret` so it is scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + let d = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + + let q = comb_multiply_base_point(&d); + // d is in [1, n-1] by construction and G has prime order n (cofactor h = 1), so [d]G is never + // the identity for any valid d -- see crate::keys_p256::keygen's identical argument. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + + Ok((ECDSASecp256K1PublicKey { x, y }, ECDSASecp256K1PrivateKey(d))) +} diff --git a/crypto/ecdsa/src/keys_p384.rs b/crypto/ecdsa/src/keys_p384.rs new file mode 100644 index 000000000..42531e9e6 --- /dev/null +++ b/crypto/ecdsa/src/keys_p384.rs @@ -0,0 +1,153 @@ +//! ECDSA P-384 key types (FIPS 186-5 §6.2) and key-pair generation (Appendix A.2.1). Identical in +//! shape to [`crate::keys_p256`] -- see that module's docs for the full reasoning -- with P-384's +//! types and SEC 1 encoding widths substituted. +//! +//! # DRBG output length +//! +//! FIPS 186-5 Table A.2 gives `p384` a required and recommended DRBG output of 384 bits for key +//! generation: `n` is close enough to `2^384` that the reduction's bias is negligible with no +//! headroom at all. But Appendix A.3.1 -- the randomised per-message secret -- requires `N + t` +//! bits with `t >= 64` regardless, so both draws are 448 bits (56 bytes) and go through +//! [`bouncycastle_ec::p384_extra_bits`]'s wide reduction; see that module's docs for the reasoning and for +//! why key generation uses the same draw. + +use crate::keys_common::DerivePublicKey; +use bouncycastle_core::errors::{RNGError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::nat; +use bouncycastle_ec::p384::P384FieldElement; +use bouncycastle_ec::p384_comb::comb_multiply_base_point; +use bouncycastle_ec::p384_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::p384_scalar::{N_LIMBS, P384Scalar}; +use bouncycastle_ec::p384_sec1; +use bouncycastle_utils::secret::Secret; +use core::fmt; +use core::fmt::{Debug, Display, Formatter}; + +/// Encoded length of a P-384 private key: SEC 1 §2.3.7 integer-to-octet-string of `d`, 48 bytes. +pub const SK_LEN: usize = 48; + +/// Encoded length of a P-384 public key in the canonical uncompressed form (SEC 1 §2.3.3, +/// `04 || X || Y`); [`ECDSAP384PublicKey::from_bytes`] also accepts the 49-byte compressed form. +pub const PK_LEN: usize = 97; + +/// Requested output length, in bytes, from the DRBG for FIPS 186-5 Appendix A.2.1 key generation +/// and Appendix A.3.1 randomised per-message secret generation: `N + t` with `N = 384` and +/// `t = 64`, the minimum A.3.1 step 2 accepts, giving 448 bits. See the module docs. +pub(crate) const EXTRA_BITS_DRBG_OUTPUT_LEN: usize = 56; + +/// An ECDSA P-384 private key: FIPS 186-5 §6.2's `d`, `d` in `[1, n-1]`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ECDSAP384PrivateKey(P384Scalar); + +impl ECDSAP384PrivateKey { + /// The wrapped scalar, for this crate's own sign implementation to compute with. + pub(crate) fn scalar(&self) -> &P384Scalar { + &self.0 + } +} + +impl DerivePublicKey for ECDSAP384PrivateKey { + fn derive_pk(&self) -> ECDSAP384PublicKey { + let q = comb_multiply_base_point(&self.0); + // d is in [1, n-1] by construction (every ECDSAP384PrivateKey is built that way; see + // from_bytes and keygen) and G has prime order n, so [d]G is never the identity. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + ECDSAP384PublicKey { x, y } + } +} + +impl SignaturePrivateKey for ECDSAP384PrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.0.to_be_bytes() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + out.fill(0); + *out = self.0.to_be_bytes(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let array: [u8; SK_LEN] = bytes.try_into().map_err(|_| { + SignatureError::DecodingError("ECDSA P-384 private key must be 48 bytes") + })?; + // FIPS 186-5 §6.2: d is in [1, n-1], checked on the raw big-endian value *before* any + // reduction. `P384Scalar::from_be_bytes` reduces mod n, so checking afterwards would accept + // an out-of-range encoding as a different, perfectly valid key: `d = n + 1` would load as + // `d = 1`, giving one key two encodings and silently treating malformed input as well- + // formed. Comparing against the fixed public values 0 and n is a one-time validation of + // caller-supplied bytes at load time, not a computation performed repeatedly on a secret + // intermediate value, so branching on it leaks nothing beyond what the caller already knows + // from having supplied these exact bytes. + let limbs = p384_sec1::limbs_from_be_bytes(&array); + let (_, borrow) = nat::sub(&limbs, &N_LIMBS); + if limbs == [0; 6] || borrow != 1 { + return Err(SignatureError::DecodingError( + "ECDSA P-384 private key must be in [1, n-1]", + )); + } + Ok(Self(P384Scalar::from_limbs(limbs))) + } +} + +/// An ECDSA P-384 public key: FIPS 186-5 §6.2's `Q = [d]G`, stored as affine coordinates. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ECDSAP384PublicKey { + pub(crate) x: P384FieldElement, + pub(crate) y: P384FieldElement, +} + +impl SignaturePublicKey for ECDSAP384PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + p384_sec1::encode_uncompressed(&self.x, &self.y) + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + out.fill(0); + *out = self.encode(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let (x, y) = p384_sec1::decode(bytes).ok_or(SignatureError::DecodingError( + "invalid or out-of-range SEC 1 P-384 point encoding", + ))?; + Ok(Self { x, y }) + } +} + +impl Display for ECDSAP384PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "ECDSAP384PublicKey {{ {:x?} }}", self.encode()) + } +} + +/// As [`keygen`], but sources the DRBG output from the caller-provided RNG, which must +/// offer a security strength of at least 192 bits: FIPS 186-5 Appendix A.2.1 step 3 requires +/// a DRBG whose security strength is "not less than" the one SP 800-57 Part 1 Rev. 5's +/// Table 2 associates with P-384's 384-bit order (`f = 384-511` in that table's ECC column, giving +/// 192-bit security). +pub fn keygen( + rng: &mut dyn RNG, +) -> Result<(ECDSAP384PublicKey, ECDSAP384PrivateKey), SignatureError> { + // FIPS 186-5 Appendix A.2.1 step 3: the DRBG must offer at least the security + // strength P-384's 384-bit order calls for (SP 800-57 Part 1 Rev. 5, Table 2: 192 bits). + if rng.security_strength() < SecurityStrength::_192bit { + return Err(SignatureError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)); + } + + // Raw DRBG output, reduced below into the private key / per-message secret: held in + // `Secret` so it is scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + let d = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + + let q = comb_multiply_base_point(&d); + // d is in [1, n-1] by construction and G has prime order n (cofactor h = 1), so [d]G is never + // the identity for any valid d -- see crate::keys_p256::keygen's identical argument. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + + Ok((ECDSAP384PublicKey { x, y }, ECDSAP384PrivateKey(d))) +} diff --git a/crypto/ecdsa/src/keys_p521.rs b/crypto/ecdsa/src/keys_p521.rs new file mode 100644 index 000000000..130207df5 --- /dev/null +++ b/crypto/ecdsa/src/keys_p521.rs @@ -0,0 +1,164 @@ +//! ECDSA P-521 key types (FIPS 186-5 §6.2) and key-pair generation (Appendix A.2.1). Identical in +//! shape to [`crate::keys_p384`] -- see that module's docs for the general reasoning -- with +//! P-521's types and SEC 1 encoding widths substituted. +//! +//! # DRBG output length, and why the reduction is the wide form +//! +//! FIPS 186-5 Table A.2 gives `p521` a required and recommended DRBG output of 521 bits for key +//! generation: `n` is within `2^260` of `2^521`, so the reduction's bias is negligible with no +//! headroom. But Appendix A.3.1 -- the randomised per-message secret -- requires `N + t` bits with +//! `t >= 64` regardless, so both draws are `N + 71 = 592` bits (74 bytes, the byte-aligned width +//! at or above `585`; [`EXTRA_BITS_DRBG_OUTPUT_LEN`]). An earlier version drew the 66-byte +//! `SK_LEN` (528 bits, `t = 7`) for both, which met Table A.2 but not A.3.1. +//! +//! Even at 528 bits the reduction had to handle a value far wider than `n`: 66 bytes is 7 bits more +//! than `n`'s 521 (SEC 1 octets are byte-aligned and 521 is not a multiple of 8), so a raw draw +//! reinterpreted as a 9-limb integer can be as large as `2^528 - 1`, far past the `< 2(n-1)` a +//! single conditional subtraction of `n - 1` assumes, and one subtraction silently leaves most of +//! the value unreduced (a bug this module's own tests caught: `keygen`/`ECDSAP521::sign` produced +//! keys/signatures too broken to round-trip through sign+verify, despite every fixed-input KAT -- +//! which never exercises a raw, unbounded DRBG value -- passing). The reduction is +//! [`bouncycastle_ec::p521_extra_bits`]'s single fold at bit 521 -- see its docs for why P-521 gets +//! neither the other curves' Barrett reduction nor the field's Solinas fold. + +use crate::keys_common::DerivePublicKey; +use bouncycastle_core::errors::{RNGError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::nat; +use bouncycastle_ec::p521::P521FieldElement; +use bouncycastle_ec::p521_comb::comb_multiply_base_point; +use bouncycastle_ec::p521_extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_ec::p521_scalar::{N_LIMBS, P521Scalar}; +use bouncycastle_ec::p521_sec1; +use bouncycastle_utils::secret::Secret; +use core::fmt; +use core::fmt::{Debug, Display, Formatter}; + +/// Encoded length of a P-521 private key: SEC 1 §2.3.7 integer-to-octet-string of `d`, 66 bytes. +pub const SK_LEN: usize = 66; + +/// Encoded length of a P-521 public key in the canonical uncompressed form (SEC 1 §2.3.3, +/// `04 || X || Y`); [`ECDSAP521PublicKey::from_bytes`] also accepts the 67-byte compressed form. +pub const PK_LEN: usize = 133; + +/// Requested output length, in bytes, from the DRBG for FIPS 186-5 Appendix A.2.1 key generation +/// and Appendix A.3.1 randomised per-message secret generation: the smallest whole number of bytes +/// holding `N + t` bits with `N = 521` and `t = 64`, i.e. `ceil(585 / 8) = 74` bytes (`t = 71`). +/// See the module docs. +pub(crate) const EXTRA_BITS_DRBG_OUTPUT_LEN: usize = 74; + +/// An ECDSA P-521 private key: FIPS 186-5 §6.2's `d`, `d` in `[1, n-1]`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ECDSAP521PrivateKey(P521Scalar); + +impl ECDSAP521PrivateKey { + /// The wrapped scalar, for this crate's own sign implementation to compute with. + pub(crate) fn scalar(&self) -> &P521Scalar { + &self.0 + } +} + +impl DerivePublicKey for ECDSAP521PrivateKey { + fn derive_pk(&self) -> ECDSAP521PublicKey { + let q = comb_multiply_base_point(&self.0); + // d is in [1, n-1] by construction (every ECDSAP521PrivateKey is built that way; see + // from_bytes and keygen) and G has prime order n, so [d]G is never the identity. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + ECDSAP521PublicKey { x, y } + } +} + +impl SignaturePrivateKey for ECDSAP521PrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.0.to_be_bytes() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + out.fill(0); + *out = self.0.to_be_bytes(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let array: [u8; SK_LEN] = bytes.try_into().map_err(|_| { + SignatureError::DecodingError("ECDSA P-521 private key must be 66 bytes") + })?; + // FIPS 186-5 §6.2: d is in [1, n-1], checked on the raw big-endian value *before* any + // reduction. `P521Scalar::from_be_bytes` reduces mod n, so checking afterwards would accept + // an out-of-range encoding as a different, perfectly valid key: `d = n + 1` would load as + // `d = 1`, giving one key two encodings and silently treating malformed input as well- + // formed. Comparing against the fixed public values 0 and n is a one-time validation of + // caller-supplied bytes at load time, not a computation performed repeatedly on a secret + // intermediate value, so branching on it leaks nothing beyond what the caller already knows + // from having supplied these exact bytes. + let limbs = p521_sec1::limbs_from_be_bytes(&array); + let (_, borrow) = nat::sub(&limbs, &N_LIMBS); + if limbs == [0; 9] || borrow != 1 { + return Err(SignatureError::DecodingError( + "ECDSA P-521 private key must be in [1, n-1]", + )); + } + Ok(Self(P521Scalar::from_limbs(limbs))) + } +} + +/// An ECDSA P-521 public key: FIPS 186-5 §6.2's `Q = [d]G`, stored as affine coordinates. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ECDSAP521PublicKey { + pub(crate) x: P521FieldElement, + pub(crate) y: P521FieldElement, +} + +impl SignaturePublicKey for ECDSAP521PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + p521_sec1::encode_uncompressed(&self.x, &self.y) + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + out.fill(0); + *out = self.encode(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let (x, y) = p521_sec1::decode(bytes).ok_or(SignatureError::DecodingError( + "invalid or out-of-range SEC 1 P-521 point encoding", + ))?; + Ok(Self { x, y }) + } +} + +impl Display for ECDSAP521PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "ECDSAP521PublicKey {{ {:x?} }}", self.encode()) + } +} + +/// As [`keygen`], but sources the DRBG output from the caller-provided RNG, which must +/// offer a security strength of at least 256 bits: FIPS 186-5 Appendix A.2.1 step 3 requires +/// a DRBG whose security strength is "not less than" the one SP 800-57 Part 1 Rev. 5's +/// Table 2 associates with P-521's 521-bit order (`f = 512+` in that table's ECC column, giving +/// 256-bit security). +pub fn keygen( + rng: &mut dyn RNG, +) -> Result<(ECDSAP521PublicKey, ECDSAP521PrivateKey), SignatureError> { + // FIPS 186-5 Appendix A.2.1 step 3: the DRBG must offer at least the security + // strength P-521's 521-bit order calls for (SP 800-57 Part 1 Rev. 5, Table 2: 256 bits). + if rng.security_strength() < SecurityStrength::_256bit { + return Err(SignatureError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)); + } + + // Raw DRBG output, reduced below into the private key / per-message secret: held in + // `Secret` so it is scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + let d = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + + let q = comb_multiply_base_point(&d); + // d is in [1, n-1] by construction and G has prime order n (cofactor h = 1), so [d]G is never + // the identity for any valid d -- see crate::keys_p256::keygen's identical argument. + let (x, y) = q.to_affine().expect("[d]G is never infinity for d in [1, n-1]"); + + Ok((ECDSAP521PublicKey { x, y }, ECDSAP521PrivateKey(d))) +} diff --git a/crypto/ecdsa/src/lib.rs b/crypto/ecdsa/src/lib.rs new file mode 100644 index 000000000..04b924c0f --- /dev/null +++ b/crypto/ecdsa/src/lib.rs @@ -0,0 +1,214 @@ +//! ECDSA over seven curves: key generation (FIPS 186-5 Appendix A.2.1), signature generation +//! (§6.4.1) and verification (§6.4.2), built on [`bouncycastle_ec`]'s field/scalar/point +//! arithmetic, SEC 1 encodings, and comb/wNAF multipliers. Each curve has one algorithm marker +//! (`ECDSAP256`, `ECDSAP384`, ...) and a key pair; the private key signs through the `core` key +//! role traits, [`SigningKey`](bouncycastle_core::signature_key::SigningKey) with a random +//! per-message `k` (§6.3.1, Appendix A.3.1) and +//! [`DeterministicSigningKey`](bouncycastle_core::signature_key::DeterministicSigningKey) with +//! `k` derived from the private key and the message hash (§6.3.2, Appendix A.3.3, RFC 6979), and +//! the public key verifies through +//! [`VerifyingKey`](bouncycastle_core::signature_key::VerifyingKey). Since the choice of `k` +//! "does not impact the signature verification process" (FIPS 186-5 §6), one verifier serves +//! both, and the RFC 6979 signer reports its own name (`ECDetDSA-P256`) from `alg_name()`. Raw +//! `r || s` is the default signature encoding; [`der`] offers DER `SEQUENCE { r, s }` (RFC 3279 +//! §2.2.3) as an alternative for interop that needs it. +//! +//! # Usage Examples +//! +//! `sign` draws a fresh `k` from the RNG it is given, so two signatures of one message differ; +//! `sign_deterministic` takes no RNG and reproduces the same signature every time: +//! +//! ``` +//! use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +//! use bouncycastle_ecdsa::keys_p256::keygen; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let (pk, sk) = keygen(&mut HashDRBG_SHA512::new_from_os())?; +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let message = b"the message to sign"; +//! +//! // ECDSA has no context string; the traits' `ctx` parameter is accepted and ignored. +//! let random = sk.sign(&mut rng, message, None)?; +//! assert_ne!(random, sk.sign(&mut rng, message, None)?, "a fresh k each time"); +//! +//! let deterministic = sk.sign_deterministic(message, None)?; +//! assert_eq!(deterministic, sk.sign_deterministic(message, None)?, "RFC 6979"); +//! +//! // One verifier for both. +//! pk.verify(message, None, &random)?; +//! pk.verify(message, None, &deterministic)?; +//! assert!(pk.verify(b"a different message", None, &random).is_err()); +//! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) +//! ``` +//! +//! Streaming, for a message you do not want to hold in memory at once. The random `k` is drawn +//! when the signer is created, so the worker itself only hashes: +//! +//! ``` +//! use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +//! use bouncycastle_core::traits::{AlgorithmInfo, SignatureSigner, SignatureVerifier}; +//! use bouncycastle_ecdsa::keys_p256::keygen; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let (pk, sk) = keygen(&mut HashDRBG_SHA512::new_from_os())?; +//! +//! let mut signer = sk.create_signer(&mut HashDRBG_SHA512::new_from_os(), None)?; +//! assert_eq!(signer.alg_name(), "ECDSA-P256"); +//! signer.do_update(b"the first chunk, "); +//! signer.do_update(b"then the second"); +//! let signature = signer.do_final()?; +//! +//! let mut verifier = pk.create_verifier(None)?; +//! verifier.do_update(b"the first chunk, then the second"); +//! verifier.do_verify_final(&signature)?; +//! +//! let mut signer = sk.create_signer_deterministic(None)?; +//! assert_eq!(signer.alg_name(), "ECDetDSA-P256"); +//! signer.do_update(b"the first chunk, then the second"); +//! pk.verify(b"the first chunk, then the second", None, &signer.do_final()?)?; +//! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) +//! ``` +//! +//! Keys encode as SEC 1 octets -- `04 || X || Y` for a public key (the 33-byte compressed form is +//! also accepted on decode), a fixed-width big-endian integer for a private key. Both reject +//! malformed input rather than reinterpreting it: +//! +//! ``` +//! use bouncycastle_rng::HashDRBG_SHA512; +//! use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +//! use bouncycastle_ecdsa::keys_p256::{ECDSAP256PrivateKey, ECDSAP256PublicKey, keygen}; +//! +//! let (pk, sk) = keygen(&mut HashDRBG_SHA512::new_from_os())?; +//! +//! let pk_bytes = pk.encode(); +//! assert_eq!(pk_bytes[0], 0x04); +//! assert_eq!(ECDSAP256PublicKey::from_bytes(&pk_bytes)?, pk); +//! +//! let sk_bytes = sk.encode(); +//! assert_eq!(ECDSAP256PrivateKey::from_bytes(&sk_bytes)?, sk); +//! +//! // `d` must be in [1, n-1]: zero, `n`, and anything above it are rejected rather than reduced. +//! assert!(ECDSAP256PrivateKey::from_bytes(&[0u8; 32]).is_err()); +//! assert!(ECDSAP256PrivateKey::from_bytes(&[0xff; 32]).is_err()); +//! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) +//! ``` +//! +//! For interop that needs DER `SEQUENCE { r, s }` rather than raw `r || s`, the keys have +//! `sign_der` / `sign_der_deterministic` and `verify_der`: +//! +//! ``` +//! use bouncycastle_ecdsa::keys_p256::keygen; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! +//! let (pk, sk) = keygen(&mut HashDRBG_SHA512::new_from_os())?; +//! let (der, der_len) = sk.sign_der(&mut HashDRBG_SHA512::new_from_os(), b"message", None)?; +//! pk.verify_der(b"message", None, &der[..der_len])?; +//! let (der, der_len) = sk.sign_der_deterministic(b"message", None)?; +//! pk.verify_der(b"message", None, &der[..der_len])?; +//! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) +//! ``` +//! +//! Every curve follows the same shape, in its own module pair: `ecdsa_p256`/`keys_p256`, +//! `ecdsa_p384`/`keys_p384`, `ecdsa_p521`/`keys_p521`, `ecdsa_p256k1`/`keys_p256k1`, +//! `ecdsa_bp256r1`/`keys_bp256r1`, `ecdsa_bp384r1`/`keys_bp384r1`, +//! `ecdsa_bp512r1`/`keys_bp512r1`. +//! +//! # Status +//! +//! Seven curves, each paired with the hash whose output matches its order: P-256 (SHA-256), P-384 +//! (SHA-384), P-521 (SHA-512), secp256k1 (SHA-256), brainpoolP256r1 (SHA-256), brainpoolP384r1 +//! (SHA-384), and brainpoolP512r1 (SHA-512). SM2 is a different signature algorithm rather than +//! another ECDSA curve, and lives in [`bouncycastle_sm2`](../bouncycastle_sm2/index.html). +//! +//! # Memory Footprint +//! +//! The following table lists the size of the on-disk bytes encoding and the in-memory struct size +//! of each curve's key objects: +//! +//! | Key Object | PK size on disk | PK size in memory | SK size on disk | SK size in memory | +//! |------------------------|-----------------|--------------------|------------------|--------------------| +//! | ECDSA P-256 | 65 | 64 | 32 | 32 | +//! | ECDSA P-384 | 97 | 96 | 48 | 48 | +//! | ECDSA P-521 | 133 | 144 | 66 | 72 | +//! | ECDSA secp256k1 | 65 | 64 | 32 | 32 | +//! | ECDSA brainpoolP256r1 | 65 | 64 | 32 | 32 | +//! | ECDSA brainpoolP384r1 | 97 | 96 | 48 | 48 | +//! | ECDSA brainpoolP512r1 | 129 | 128 | 64 | 64 | +//! +//! All values are in bytes. The "in memory" sizes are measured by rust's `std::mem::size_of`; the +//! "on disk" sizes are each curve's `PK_LEN`/`SK_LEN`. P-521's odd numbers come from its 521-bit +//! (not byte-aligned) field: a coordinate needs 66 bytes on disk (`ceil(521/8)`) but a `P521FieldElement` +//! is stored in 9 `u64` limbs (72 bytes) rather than a tighter packing. These numbers are produced +//! by `mem_usage_benches`' `bench_ecdsa_mem_usage` binary's `print_struct_sizes()`; that binary is +//! also the stack-usage measurement harness (see its own doc comment for the `valgrind`/`massif` +//! invocation). +//! +//! # 🚨 Security Considerations 🚨 +//! +//! - **Everything done with the private key `d` and the accepted per-message secret `k` is +//! branch-free and does not index memory by their value**: inversion, multiplication, the +//! fixed-base scalar multiplication `[k]G`. `d` lives in +//! [`bouncycastle_ec::p256_scalar::P256Scalar`] (a [`bouncycastle_utils::secret::Secret`]); see +//! [`bouncycastle_ec`]'s own module docs for the constant-time discipline this crate builds on +//! and the exceptions it documents. This crate adds two exceptions of its own: +//! - The RFC 6979 candidate loop of the deterministic signers runs a data-dependent number of +//! iterations (the random-`k` signers draw `k` per FIPS 186-5 Appendix A.3.1, which has no +//! rejection loop). On the NIST curves +//! and secp256k1 a candidate is rejected with negligible probability; on the brainpool curves +//! 33 % to 45 % of signatures reject at least one. Under the PRF assumption the count is +//! independent of the accepted `k` and reveals nothing about `d`, but it is deterministic per +//! `(d, message)`, so a remote observer gets a per-message timing fingerprint. See +//! [`rfc6979_p256::generate_k`]'s docs for the argument, the brainpool modules' for the rates, and +//! [`rfc6979_bp256r1::generate_k`]'s for what the fingerprint is worth. +//! - On `r = 0` or `s = 0`, FIPS 186-5 §6.4.1 step 11 has the random-`k` signer draw another +//! `k` and the deterministic one output failure (RFC 6979 §3.4 would continue its loop +//! instead). Both signers here report failure: the random `k` is drawn when the signer is +//! created and there is no RNG at `do_final` to draw another from, so the caller makes a fresh +//! signer. Either branch is on a public outcome, taken with probability about `2^-(len(n)-1)`. +//! - **`k` must never repeat across two signatures under the same key with different messages.** +//! A repeated `k` leaks `d` directly from two signatures' `(r, s)` pairs (elementary algebra on +//! the ECDSA equation). The random-`k` signer therefore depends on the RNG handed to +//! `create_signer` / `sign` being correctly seeded for every signature. The deterministic signer +//! removes that dependency: `k` is a function of `d` and the message hash, so the same +//! `(d, message)` pair always regenerates the same `k` and distinct messages give distinct `k` +//! (FIPS 186-5 §6.3.2: "This protects against attacks arising from generating signatures with +//! insufficient randomness in the per-message secret number"). +//! - **Deterministic signatures are a fault-attack target** (FIPS 186-5 §6, first paragraph): an +//! attacker who can induce a computational fault and observe the faulty signature may be able to +//! recover `d`, because a deterministic scheme will reproduce the exact same `k` on a retry. This +//! is a property of determinism itself, not a defect in this implementation; see FIPS 186-5's +//! references \[17\]-\[22\] for mitigations at the deployment level (e.g. verifying a signature +//! in-device before releasing it). Where that attacker is in the threat model and the RNG is +//! trusted, the random-`k` signer (`SigningKey`) is the one to use. +//! - **`ctx` is accepted but ignored.** ECDSA (FIPS 186-5, SEC 1) has no context-string input; +//! the key traits' `ctx` parameter is accepted for trait conformance and silently discarded. A +//! caller relying on `ctx` to bind a signature to an application context gets no such binding +//! from ECDSA. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +pub mod der; +pub mod ecdsa_bp256r1; +pub mod ecdsa_bp384r1; +pub mod ecdsa_bp512r1; +pub mod ecdsa_p256; +pub mod ecdsa_p256k1; +pub mod ecdsa_p384; +pub mod ecdsa_p521; +pub mod keys_bp256r1; +pub mod keys_bp384r1; +pub mod keys_bp512r1; +pub mod keys_common; +pub mod keys_p256; +pub mod keys_p256k1; +pub mod keys_p384; +pub mod keys_p521; +pub mod rfc6979_bp256r1; +pub mod rfc6979_bp384r1; +pub mod rfc6979_bp512r1; +pub mod rfc6979_p256; +pub mod rfc6979_p256k1; +pub mod rfc6979_p384; +pub mod rfc6979_p521; diff --git a/crypto/ecdsa/src/rfc6979_bp256r1.rs b/crypto/ecdsa/src/rfc6979_bp256r1.rs new file mode 100644 index 000000000..f3c3df350 --- /dev/null +++ b/crypto/ecdsa/src/rfc6979_bp256r1.rs @@ -0,0 +1,145 @@ +//! RFC 6979 §3.2 / FIPS 186-5 Appendix A.3.3: the deterministic per-message secret `k` for +//! brainpoolP256r1, fixed to SHA-256 (the pairing wycheproof's `ecdsa_brainpoolP256r1_sha256_*` +//! vector files use). Identical in shape to [`crate::rfc6979_p256`] (P-256) -- see that module's docs +//! for the full reasoning -- since brainpoolP256r1's `n` is, like P-256's, 256 bits: `HLEN` +//! (SHA-256's output) and `qlen` are equal, so the same `int2octets`/`bits2octets`/single-HMAC- +//! round shortcuts apply. `bits2octets`'s single-conditional-subtraction reduction is valid for +//! the same reason as secp256k1's: brainpoolP256r1's `n` also has its top bit set (`n > +//! 2^255`), so `2n > 2^256` and any 256-bit input is already `< 2n` -- true regardless of how +//! close `n` is to `2^256` itself (unlike secp256k1, brainpoolP256r1's `n` is not close to a power +//! of two at all; only the top-bit-set property matters here). +//! +//! `n` not being close to `2^256` also makes the step h.3 rejection loop a normal control path on +//! this curve, not the near-dead branch it is on the NIST curves and secp256k1; [`generate_k`]'s +//! docs give the rate and what it amounts to. RFC 6979 puts no probability on this loop (its +//! "utterly improbable", §2.4 and §3.4, is about `r = 0`). +//! +//! RFC 6979 has no official Appendix A.2.x-style test vectors for brainpool curves (its appendix +//! only covers the NIST curves P-192 through P-521); `rfc6979_bp256r1_vectors_tests.rs` +//! cross-checks this implementation against an independent Python implementation of the same RFC +//! instead. + +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle_core::traits::MAC; +use bouncycastle_ec::bp256r1_scalar::{Bp256r1Scalar, N_LIMBS}; +use bouncycastle_ec::bp256r1_sec1; +use bouncycastle_ec::nat; +use bouncycastle_hmac::HmacKey; +use bouncycastle_sha2::SHA256; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +const HLEN: usize = 32; + +const _: () = + assert!(HLEN * 8 == 256, "int2octets/bits2octets shortcuts assume hlen == qlen == 256"); + +/// `HMAC_K(data)` (§3.1.1). See [`crate::rfc6979_p256::hmac_k`]'s docs. +/// As [`crate::rfc6979_p256::hmac_k`], including why this returns a [`Secret`] rather than a bare +/// array: see that function's docs and [`generate_k`]'s "What is scrubbed" section. +fn hmac_k(key: &[u8; HLEN], data: &[&[u8]]) -> Secret<[u8; HLEN]> { + let key_material = KeyMaterial::::from_bytes_as_type(key, KeyType::MACKey) + .expect("HLEN-byte key always fits"); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key_material) + .expect("weak key always allowed"); + let mut hmac = hmac_key.create_generator(); + for piece in data { + hmac.do_update(piece); + } + let mut out = Secret::<[u8; HLEN]>::new(); + hmac.do_final_out(&mut *out).expect("HLEN-byte output buffer always fits HMAC-SHA256's output"); + out +} + +/// RFC 6979 §2.3.4 `bits2octets(h1)`. See [`crate::rfc6979_p256::bits2octets`]'s docs -- the same +/// single-subtraction argument applies here (see this module's own docs for why it holds despite +/// brainpoolP256r1's `n` not being close to `2^256`). +fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { + let limbs = bp256r1_sec1::limbs_from_be_bytes(h1); + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + bp256r1_sec1::be_bytes_from_limbs(&reduced) +} + +/// `true` iff the raw (unreduced) candidate bytes, read as a big-endian integer, are in `[1, +/// n-1]` -- RFC 6979 step h.3's comparison against `q`, by [`crate::rfc6979_p256::limbs_in_range`]. +fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { + crate::rfc6979_p256::limbs_in_range(&bp256r1_sec1::limbs_from_be_bytes(candidate), &N_LIMBS) +} + +/// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and +/// message hash `h1 = H(m)`. +/// +/// # The rejection loop is a normal path on the brainpool curves +/// +/// A candidate is rejected with probability `1 - n/2^256 = 0.336`, so 34 % of signatures run step +/// h.3's "otherwise" branch at least once (mean 0.51 extra iterations); [`crate::rfc6979_bp384r1`] +/// and [`crate::rfc6979_bp512r1`] state their own rates. Each extra iteration is three more HMAC +/// calls (two for the re-key, one for the next candidate) -- a timing difference a remote observer +/// can see. [`crate::rfc6979_p256::generate_k`]'s docs give the argument for why the count reveals +/// nothing about `d` or the accepted `k`; it does not depend on the rate. What the rate changes is +/// the practical consequence: the count is deterministic per `(d, msg)`, so the timing pattern over +/// chosen messages is a fingerprint worth one to two bits per message -- useless for recovering +/// `d`, usable for confirming a candidate `d` an attacker already holds. +pub fn generate_k(d: &Bp256r1Scalar, h1: &[u8; HLEN]) -> Bp256r1Scalar { + let mut int2octets_d = Secret::<[u8; HLEN]>::new(); + *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] + let bits2octets_h1 = bits2octets(h1); + + // step c + let mut key = Secret::<[u8; HLEN]>::new(); + // step b + let mut v = Secret::<[u8; HLEN]>::new(); + *v = [0x01u8; HLEN]; + + // step d + key = hmac_k(&key, &[&*v, &[0x00], &*int2octets_d, &bits2octets_h1]); + // step e + v = hmac_k(&key, &[&*v]); + // step f + key = hmac_k(&key, &[&*v, &[0x01], &*int2octets_d, &bits2octets_h1]); + // step g + v = hmac_k(&key, &[&*v]); + + // step h + loop { + // step h.1-h.2: T = HMAC_K(V) suffices in one round since tlen == qlen == HLEN*8 (see + // module docs) + v = hmac_k(&key, &[&*v]); + if candidate_in_range(&v) { + // in [1, n-1] already: from_limbs's reduction is a no-op safety net, not a real reduce + return Bp256r1Scalar::from_limbs(bp256r1_sec1::limbs_from_be_bytes(&v)); + } + key = hmac_k(&key, &[&*v, &[0x00]]); + v = hmac_k(&key, &[&*v]); + } +} + +// `candidate_in_range`'s reject path is covered from outside the crate, by the rejecting cases in +// `rfc6979_bp256r1_vectors_tests.rs`; what those cannot pin are the exact boundaries, which no +// realistic input lands on: `0` and `n`, the known answers below. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn candidate_in_range_boundary_cases() { + assert!(!candidate_in_range(&[0u8; HLEN])); + + let mut one = [0u8; HLEN]; + one[HLEN - 1] = 1; + assert!(candidate_in_range(&one)); + + let n_bytes = bp256r1_sec1::be_bytes_from_limbs(&N_LIMBS); + assert!(!candidate_in_range(&n_bytes)); + + let mut n_minus_1_bytes = n_bytes; + *n_minus_1_bytes.last_mut().unwrap() -= 1; // N_LIMBS[0] is odd, so this never borrows + assert!(candidate_in_range(&n_minus_1_bytes)); + + assert!(!candidate_in_range(&[0xffu8; HLEN])); + } +} diff --git a/crypto/ecdsa/src/rfc6979_bp384r1.rs b/crypto/ecdsa/src/rfc6979_bp384r1.rs new file mode 100644 index 000000000..bf8bf3e75 --- /dev/null +++ b/crypto/ecdsa/src/rfc6979_bp384r1.rs @@ -0,0 +1,136 @@ +//! RFC 6979 §3.2 / FIPS 186-5 Appendix A.3.3: the deterministic per-message secret `k` for +//! brainpoolP384r1, fixed to SHA-384 (the pairing wycheproof's `ecdsa_brainpoolP384r1_sha384_*` +//! vector files use). Identical in shape to [`crate::rfc6979_p384`] -- see that module's docs for +//! the full derivation -- with `HLEN = 48` (SHA-384's output) and brainpoolP384r1's types +//! substituted. `qlen == hlen == 384` here too, so the same shortcuts collapse the same way. +//! `bits2octets`'s single-conditional-subtraction reduction is valid for the same reason as +//! brainpoolP256r1's own case: brainpoolP384r1's `n` has its top bit set (`n > 2^383`), so `2n > +//! 2^384` and any 384-bit input is already `< 2n`, despite `n` not being close to `2^384` itself. +//! +//! `n` not being close to `2^384` also makes the step h.3 rejection loop a normal control path on +//! this curve, the most-taken in the crate; [`generate_k`]'s docs give the rate. +//! +//! RFC 6979 has no official Appendix A.2.x-style test vectors for brainpool curves; +//! `rfc6979_bp384r1_vectors_tests.rs` cross-checks this implementation against an independent +//! Python implementation of the same RFC instead. + +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle_core::traits::MAC; +use bouncycastle_ec::bp384r1_scalar::{Bp384r1Scalar, N_LIMBS}; +use bouncycastle_ec::bp384r1_sec1; +use bouncycastle_ec::nat; +use bouncycastle_hmac::HmacKey; +use bouncycastle_sha2::SHA384; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +const HLEN: usize = 48; + +const _: () = + assert!(HLEN * 8 == 384, "int2octets/bits2octets shortcuts assume hlen == qlen == 384"); + +/// `HMAC_K(data)` (§3.1.1). See [`crate::rfc6979_p256::hmac_k`]'s docs. +/// As [`crate::rfc6979_p256::hmac_k`], including why this returns a [`Secret`] rather than a bare +/// array: see that function's docs and [`generate_k`]'s "What is scrubbed" section. +fn hmac_k(key: &[u8; HLEN], data: &[&[u8]]) -> Secret<[u8; HLEN]> { + let key_material = KeyMaterial::::from_bytes_as_type(key, KeyType::MACKey) + .expect("HLEN-byte key always fits"); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key_material) + .expect("weak key always allowed"); + let mut hmac = hmac_key.create_generator(); + for piece in data { + hmac.do_update(piece); + } + let mut out = Secret::<[u8; HLEN]>::new(); + hmac.do_final_out(&mut *out).expect("HLEN-byte output buffer always fits HMAC-SHA384's output"); + out +} + +/// RFC 6979 §2.3.4 `bits2octets(h1)`. See [`crate::rfc6979_p256::bits2octets`]'s docs -- the same +/// single-subtraction argument applies here (see this module's own docs for why it holds despite +/// brainpoolP384r1's `n` not being close to `2^384`). +fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { + let limbs = bp384r1_sec1::limbs_from_be_bytes(h1); + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + bp384r1_sec1::be_bytes_from_limbs(&reduced) +} + +/// `true` iff the raw (unreduced) candidate bytes, read as a big-endian integer, are in `[1, +/// n-1]` -- RFC 6979 step h.3's comparison against `q`, by [`crate::rfc6979_p256::limbs_in_range`]. +fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { + crate::rfc6979_p256::limbs_in_range(&bp384r1_sec1::limbs_from_be_bytes(candidate), &N_LIMBS) +} + +/// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and +/// message hash `h1 = H(m)`. +/// +/// # The rejection loop is a normal path on this curve +/// +/// A candidate is rejected with probability `1 - n/2^384 = 0.4503`, so 45 % of signatures run step +/// h.3's "otherwise" branch at least once and a fifth at least twice (mean 0.82 extra iterations), +/// each three more HMAC-SHA-384 calls. What that count is worth to an observer, and why it reveals +/// nothing about `d`: [`crate::rfc6979_bp256r1::generate_k`]'s docs, which hold for every brainpool +/// curve. +pub fn generate_k(d: &Bp384r1Scalar, h1: &[u8; HLEN]) -> Bp384r1Scalar { + let mut int2octets_d = Secret::<[u8; HLEN]>::new(); + *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] + let bits2octets_h1 = bits2octets(h1); + + // step c + let mut key = Secret::<[u8; HLEN]>::new(); + // step b + let mut v = Secret::<[u8; HLEN]>::new(); + *v = [0x01u8; HLEN]; + + // step d + key = hmac_k(&key, &[&*v, &[0x00], &*int2octets_d, &bits2octets_h1]); + // step e + v = hmac_k(&key, &[&*v]); + // step f + key = hmac_k(&key, &[&*v, &[0x01], &*int2octets_d, &bits2octets_h1]); + // step g + v = hmac_k(&key, &[&*v]); + + // step h + loop { + // step h.1-h.2: T = HMAC_K(V) suffices in one round since tlen == qlen == HLEN*8 (see + // module docs) + v = hmac_k(&key, &[&*v]); + if candidate_in_range(&v) { + // in [1, n-1] already: from_limbs's reduction is a no-op safety net, not a real reduce + return Bp384r1Scalar::from_limbs(bp384r1_sec1::limbs_from_be_bytes(&v)); + } + key = hmac_k(&key, &[&*v, &[0x00]]); + v = hmac_k(&key, &[&*v]); + } +} + +// `candidate_in_range`'s reject path is covered from outside the crate, by the rejecting cases in +// `rfc6979_bp384r1_vectors_tests.rs`; what those cannot pin are the exact boundaries, which no +// realistic input lands on: `0` and `n`, the known answers below. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn candidate_in_range_boundary_cases() { + assert!(!candidate_in_range(&[0u8; HLEN])); + + let mut one = [0u8; HLEN]; + one[HLEN - 1] = 1; + assert!(candidate_in_range(&one)); + + let n_bytes = bp384r1_sec1::be_bytes_from_limbs(&N_LIMBS); + assert!(!candidate_in_range(&n_bytes)); + + let mut n_minus_1_bytes = n_bytes; + *n_minus_1_bytes.last_mut().unwrap() -= 1; // N_LIMBS[0] is odd, so this never borrows + assert!(candidate_in_range(&n_minus_1_bytes)); + + assert!(!candidate_in_range(&[0xffu8; HLEN])); + } +} diff --git a/crypto/ecdsa/src/rfc6979_bp512r1.rs b/crypto/ecdsa/src/rfc6979_bp512r1.rs new file mode 100644 index 000000000..90a2400f2 --- /dev/null +++ b/crypto/ecdsa/src/rfc6979_bp512r1.rs @@ -0,0 +1,136 @@ +//! RFC 6979 §3.2 / FIPS 186-5 Appendix A.3.3: the deterministic per-message secret `k` for +//! brainpoolP512r1, fixed to SHA-512 (the pairing wycheproof's `ecdsa_brainpoolP512r1_sha512_*` +//! vector files use). Identical in shape to [`crate::rfc6979_p384`] -- see that module's docs for +//! the full derivation -- with `HLEN = 64` (SHA-512's output) and brainpoolP512r1's types +//! substituted. `qlen == hlen == 512` here (this curve's `n` is 512 bits, unlike P-521's own +//! 521-bit `n` paired with the same hash), so the same shortcuts collapse the same way. +//! `bits2octets`'s single-conditional-subtraction reduction is valid for the same reason as the +//! other brainpool curves' own case: brainpoolP512r1's `n` has its top bit set (`n > 2^511`), so +//! `2n > 2^512` and any 512-bit input is already `< 2n`, despite `n` not being close to `2^512`. +//! +//! `n` not being close to `2^512` also makes the step h.3 rejection loop a normal control path on +//! this curve; [`generate_k`]'s docs give the rate. +//! +//! RFC 6979 has no official Appendix A.2.x-style test vectors for brainpool curves; +//! `rfc6979_bp512r1_vectors_tests.rs` cross-checks this implementation against an independent +//! Python implementation of the same RFC instead. + +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle_core::traits::MAC; +use bouncycastle_ec::bp512r1_scalar::{Bp512r1Scalar, N_LIMBS}; +use bouncycastle_ec::bp512r1_sec1; +use bouncycastle_ec::nat; +use bouncycastle_hmac::HmacKey; +use bouncycastle_sha2::SHA512; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +const HLEN: usize = 64; + +const _: () = + assert!(HLEN * 8 == 512, "int2octets/bits2octets shortcuts assume hlen == qlen == 512"); + +/// `HMAC_K(data)` (§3.1.1). See [`crate::rfc6979_p256::hmac_k`]'s docs. +/// As [`crate::rfc6979_p256::hmac_k`], including why this returns a [`Secret`] rather than a bare +/// array: see that function's docs and [`generate_k`]'s "What is scrubbed" section. +fn hmac_k(key: &[u8; HLEN], data: &[&[u8]]) -> Secret<[u8; HLEN]> { + let key_material = KeyMaterial::::from_bytes_as_type(key, KeyType::MACKey) + .expect("HLEN-byte key always fits"); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key_material) + .expect("weak key always allowed"); + let mut hmac = hmac_key.create_generator(); + for piece in data { + hmac.do_update(piece); + } + let mut out = Secret::<[u8; HLEN]>::new(); + hmac.do_final_out(&mut *out).expect("HLEN-byte output buffer always fits HMAC-SHA512's output"); + out +} + +/// RFC 6979 §2.3.4 `bits2octets(h1)`. See [`crate::rfc6979_p256::bits2octets`]'s docs -- the same +/// single-subtraction argument applies here (see this module's own docs for why it holds despite +/// brainpoolP512r1's `n` not being close to `2^512`). +fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { + let limbs = bp512r1_sec1::limbs_from_be_bytes(h1); + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 8]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + bp512r1_sec1::be_bytes_from_limbs(&reduced) +} + +/// `true` iff the raw (unreduced) candidate bytes, read as a big-endian integer, are in `[1, +/// n-1]` -- RFC 6979 step h.3's comparison against `q`, by [`crate::rfc6979_p256::limbs_in_range`]. +fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { + crate::rfc6979_p256::limbs_in_range(&bp512r1_sec1::limbs_from_be_bytes(candidate), &N_LIMBS) +} + +/// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and +/// message hash `h1 = H(m)`. +/// +/// # The rejection loop is a normal path on this curve +/// +/// A candidate is rejected with probability `1 - n/2^512 = 0.3326`, so 33 % of signatures run step +/// h.3's "otherwise" branch at least once (mean 0.50 extra iterations), each three more +/// HMAC-SHA-512 calls. What that count is worth to an observer, and why it reveals nothing about +/// `d`: [`crate::rfc6979_bp256r1::generate_k`]'s docs, which hold for every brainpool curve. +pub fn generate_k(d: &Bp512r1Scalar, h1: &[u8; HLEN]) -> Bp512r1Scalar { + let mut int2octets_d = Secret::<[u8; HLEN]>::new(); + *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] + let bits2octets_h1 = bits2octets(h1); + + // step c + let mut key = Secret::<[u8; HLEN]>::new(); + // step b + let mut v = Secret::<[u8; HLEN]>::new(); + *v = [0x01u8; HLEN]; + + // step d + key = hmac_k(&key, &[&*v, &[0x00], &*int2octets_d, &bits2octets_h1]); + // step e + v = hmac_k(&key, &[&*v]); + // step f + key = hmac_k(&key, &[&*v, &[0x01], &*int2octets_d, &bits2octets_h1]); + // step g + v = hmac_k(&key, &[&*v]); + + // step h + loop { + // step h.1-h.2: T = HMAC_K(V) suffices in one round since tlen == qlen == HLEN*8 (see + // module docs) + v = hmac_k(&key, &[&*v]); + if candidate_in_range(&v) { + // in [1, n-1] already: from_limbs's reduction is a no-op safety net, not a real reduce + return Bp512r1Scalar::from_limbs(bp512r1_sec1::limbs_from_be_bytes(&v)); + } + key = hmac_k(&key, &[&*v, &[0x00]]); + v = hmac_k(&key, &[&*v]); + } +} + +// `candidate_in_range`'s reject path is covered from outside the crate, by the rejecting cases in +// `rfc6979_bp512r1_vectors_tests.rs`; what those cannot pin are the exact boundaries, which no +// realistic input lands on: `0` and `n`, the known answers below. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn candidate_in_range_boundary_cases() { + assert!(!candidate_in_range(&[0u8; HLEN])); + + let mut one = [0u8; HLEN]; + one[HLEN - 1] = 1; + assert!(candidate_in_range(&one)); + + let n_bytes = bp512r1_sec1::be_bytes_from_limbs(&N_LIMBS); + assert!(!candidate_in_range(&n_bytes)); + + let mut n_minus_1_bytes = n_bytes; + *n_minus_1_bytes.last_mut().unwrap() -= 1; // N_LIMBS[0] is odd, so this never borrows + assert!(candidate_in_range(&n_minus_1_bytes)); + + assert!(!candidate_in_range(&[0xffu8; HLEN])); + } +} diff --git a/crypto/ecdsa/src/rfc6979_p256.rs b/crypto/ecdsa/src/rfc6979_p256.rs new file mode 100644 index 000000000..c53d544a9 --- /dev/null +++ b/crypto/ecdsa/src/rfc6979_p256.rs @@ -0,0 +1,183 @@ +//! RFC 6979 §3.2 / FIPS 186-5 Appendix A.3.3: the deterministic per-message secret `k`, fixed to +//! SHA-256 (this crate's only supported hash so far -- see [`crate`]'s module docs). Quotes below +//! are RFC 6979 §3.2's own lettered steps; FIPS 186-5 A.3.3 restates the same process in +//! HMAC_DRBG terms (steps 1.1-1.9, 2-5), cited alongside where the two differ in presentation. +//! +//! `HLEN` (SHA-256's output) and `qlen` (`n`'s bit length, 256 for P-256) are equal here, which +//! collapses two of RFC 6979's general steps: `int2octets`/`bits2octets` (§2.3.3/§2.3.4) need no +//! truncation-or-padding step because a 32-byte value already has exactly `rlen = 256` bits, and +//! step h.2's "while tlen < qlen" loop always runs its body exactly once, since one `HMAC_K(V)` +//! already produces `qlen` bits of `T`. Neither shortcut is taken silently: the `const` assertion +//! below fails to compile if this crate ever gains a curve/hash pairing where they don't hold. + +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle_core::traits::MAC; +use bouncycastle_ec::nat; +use bouncycastle_ec::p256_scalar::{N_LIMBS, P256Scalar}; +use bouncycastle_ec::p256_sec1; +use bouncycastle_hmac::HmacKey; +use bouncycastle_sha2::SHA256; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +const HLEN: usize = 32; + +const _: () = + assert!(HLEN * 8 == 256, "int2octets/bits2octets shortcuts assume hlen == qlen == 256"); + +/// `HMAC_K(data)` (§3.1.1), `data` passed as multiple pieces so callers don't need to concatenate +/// into one buffer first. +/// +/// Returns a [`Secret`], not a bare array: every value this produces is HMAC_DRBG state derived +/// from the private key, and the last one produced *is* `k`. Returning `Secret` means each is +/// scrubbed when it is dropped -- including the previous `key`/`v` that [`generate_k`]'s next step +/// overwrites -- without disturbing that function's one-line-per-RFC-step shape. +/// `from_key_material_allow_weak`: every `K` used here is 32 synthetic bytes from this same construction +/// (starting from `0x00...00` in step c), not an application key, so there is no real "security +/// strength" for [`KeyMaterial::from_bytes`] to have under- or over-estimated. +fn hmac_k(key: &[u8; HLEN], data: &[&[u8]]) -> Secret<[u8; HLEN]> { + let key_material = KeyMaterial::::from_bytes_as_type(key, KeyType::MACKey) + .expect("HLEN-byte key always fits"); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key_material) + .expect("weak key always allowed"); + let mut hmac = hmac_key.create_generator(); + for piece in data { + hmac.do_update(piece); + } + let mut out = Secret::<[u8; HLEN]>::new(); + hmac.do_final_out(&mut *out).expect("HLEN-byte output buffer always fits HMAC-SHA256's output"); + out +} + +/// RFC 6979 §2.3.4 `bits2octets(h1)`: reduce the message hash `h1` mod `n`, then §2.3.3 +/// `int2octets`. `h1` is `bits2int`'d as-is (no truncation: `blen == qlen`), so this is one +/// conditional subtraction -- the same single-subtraction argument +/// [`P256Scalar::from_limbs`](bouncycastle_ec::p256_scalar::P256Scalar::from_limbs) documents for +/// any 256-bit input against a modulus this close to `2^256`. `h1` is a public message hash, not a +/// secret, so there is no constant-time obligation here; the branch-free form is used anyway to +/// match this crate's house style. +fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { + let limbs = p256_sec1::limbs_from_be_bytes(h1); + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + p256_sec1::be_bytes_from_limbs(&reduced) +} + +/// `true` iff `limbs` is in `[1, n-1]`: every curve's step h.3 range check. +/// +/// The comparison is on the candidate's own bits, so it is done as masks and converted with +/// `Condition::to_bool`, which tests the combined mask after the optimisation barrier: the one +/// branch is `generate_k`'s loop decision, which the crate docs list as a timing exception. +pub(crate) fn limbs_in_range(limbs: &[u64; L], n: &[u64; L]) -> bool { + let is_nonzero = !nat::is_zero(limbs); + let (_, borrow) = nat::sub(limbs, n); + (is_nonzero & Condition::::from_lsb(borrow)).to_bool() +} + +/// `true` iff the raw (unreduced) candidate bytes, read as a big-endian integer, are in `[1, +/// n-1]` -- RFC 6979 step h.3's comparison against `q`, "not reduced modulo q" (§3.2, footnote to +/// step h). +fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { + limbs_in_range(&p256_sec1::limbs_from_be_bytes(candidate), &N_LIMBS) +} + +/// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and +/// message hash `h1 = H(m)`. +/// +/// # What is scrubbed +/// +/// `int2octets(d)`, `key` and `v` are all derived from the private key (and `v` ends up holding +/// `k` itself), so all three live in [`Secret`] and are erased when this function returns rather +/// than left on the stack for whatever runs next -- matching how `bouncycastle_mlkem` and +/// `bouncycastle_mldsa` treat their own secret stack intermediates. `bits2octets(h1)` is not +/// wrapped: it derives from the public message hash, not from `d`. +/// +/// One copy is outside this function's reach: `d.to_be_bytes()` hands back a plain array by +/// design (see [`P256Scalar::to_be_bytes`](bouncycastle_ec::p256_scalar::P256Scalar::to_be_bytes), +/// whose own docs note it "momentarily holds the plain value ... the caller asked for the bytes"), +/// so that transient is copied into the `Secret` above but the transient itself is not erased. +/// +/// # The rejection loop +/// +/// The rejection loop (step h.3, "otherwise ... loop") is the one place this crate's own +/// constant-time rules permit branching on a value derived from a secret; the crate docs list it as +/// an exception. The argument, which applies equally to FIPS 186-5 Appendix A.3.2's and A.4.2's +/// identical loops: the iteration count is a function of the HMAC-DRBG output stream, i.e. of PRF +/// outputs under a key derived from `(d, h1)`, so under the PRF assumption it is independent of the +/// accepted `k` -- which, conditioned on the count, is uniform on `[1, n-1]` -- and reveals nothing +/// about `d`. It does not rely on a rejected candidate being discarded, which RFC 6979's chaining +/// never satisfies: after `V` is rejected, `K' = HMAC_K(V || 0x00)` and `V' = HMAC_K'(V)`, and the +/// accepted `k` is `bits2int(HMAC_K'(V'))`. What must be constant time is everything subsequently +/// done *with* the accepted `k`, which is the rest of this crate's concern. The same argument +/// covers every curve in this crate; what differs is the rate. For P-256 the loop is close to dead: +/// `2^256 - n` is about `2^224`, so a candidate is rejected with probability about `2^-32`. The +/// brainpool modules, where it is a normal path, state their rates; +/// [`crate::rfc6979_bp256r1::generate_k`]'s docs say what the count then amounts to in practice. +/// +/// The loop rejects only out-of-range candidates. RFC 6979 §3.4 would also loop on a `k` giving +/// `r = 0`; this crate follows FIPS 186-5 §6.4.1 step 11 instead, under which deterministic signing +/// outputs failure there (see the crate docs' Security Considerations). +pub fn generate_k(d: &P256Scalar, h1: &[u8; HLEN]) -> P256Scalar { + let mut int2octets_d = Secret::<[u8; HLEN]>::new(); + *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] + let bits2octets_h1 = bits2octets(h1); + + // step c + let mut key = Secret::<[u8; HLEN]>::new(); + // step b + let mut v = Secret::<[u8; HLEN]>::new(); + *v = [0x01u8; HLEN]; + + // step d + key = hmac_k(&key, &[&*v, &[0x00], &*int2octets_d, &bits2octets_h1]); + // step e + v = hmac_k(&key, &[&*v]); + // step f + key = hmac_k(&key, &[&*v, &[0x01], &*int2octets_d, &bits2octets_h1]); + // step g + v = hmac_k(&key, &[&*v]); + + // step h + loop { + // step h.1-h.2: T = HMAC_K(V) suffices in one round since tlen == qlen == HLEN*8 (see + // module docs) + v = hmac_k(&key, &[&*v]); + if candidate_in_range(&v) { + // in [1, n-1] already: from_limbs's reduction is a no-op safety net, not a real reduce + return P256Scalar::from_limbs(p256_sec1::limbs_from_be_bytes(&v)); + } + key = hmac_k(&key, &[&*v, &[0x00]]); + v = hmac_k(&key, &[&*v]); + } +} + +// `candidate_in_range` is private and, through `generate_k`'s rejection loop, only ever reached on +// candidates that -- for any real message/key input -- are essentially always in range (a uniform +// 256-bit candidate is `>= n` with probability about `2^-32`), so no realistic integration test +// exercises its reject path; the QUALITY_AND_STYLE.md private-function exception applies. Known +// answers: `0` and `n` are the immediate out-of-range neighbours of the valid interval `[1, n-1]`. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn candidate_in_range_boundary_cases() { + assert!(!candidate_in_range(&[0u8; HLEN])); + + let mut one = [0u8; HLEN]; + one[HLEN - 1] = 1; + assert!(candidate_in_range(&one)); + + let n_bytes = p256_sec1::be_bytes_from_limbs(&N_LIMBS); + assert!(!candidate_in_range(&n_bytes)); + + let mut n_minus_1_bytes = n_bytes; + *n_minus_1_bytes.last_mut().unwrap() -= 1; // N_LIMBS[0] is odd, so this never borrows + assert!(candidate_in_range(&n_minus_1_bytes)); + + assert!(!candidate_in_range(&[0xffu8; HLEN])); + } +} diff --git a/crypto/ecdsa/src/rfc6979_p256k1.rs b/crypto/ecdsa/src/rfc6979_p256k1.rs new file mode 100644 index 000000000..0a93781e8 --- /dev/null +++ b/crypto/ecdsa/src/rfc6979_p256k1.rs @@ -0,0 +1,127 @@ +//! RFC 6979 §3.2 / FIPS 186-5 Appendix A.3.3: the deterministic per-message secret `k` for +//! secp256k1, fixed to SHA-256 (the conventional pairing for this curve -- e.g. Bitcoin's own +//! usage, and wycheproof's `ecdsa_secp256k1_sha256_*` vector files). Identical in shape to +//! [`crate::rfc6979_p256`] (P-256) -- see that module's docs for the full reasoning -- since +//! secp256k1's `n` is, like P-256's, 256 bits: `HLEN` (SHA-256's output) and `qlen` are equal, so +//! the same `int2octets`/`bits2octets`/single-HMAC-round shortcuts apply. +//! +//! RFC 6979 has no official Appendix A.2.x-style test vectors for secp256k1 (its appendix only +//! covers the NIST curves P-192 through P-521); `rfc6979_p256k1_vectors_tests.rs` cross-checks this +//! implementation against an independent Python implementation of the same RFC instead. + +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle_core::traits::MAC; +use bouncycastle_ec::nat; +use bouncycastle_ec::p256k1_scalar::{N_LIMBS, P256K1Scalar}; +use bouncycastle_ec::p256k1_sec1; +use bouncycastle_hmac::HmacKey; +use bouncycastle_sha2::SHA256; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +const HLEN: usize = 32; + +const _: () = + assert!(HLEN * 8 == 256, "int2octets/bits2octets shortcuts assume hlen == qlen == 256"); + +/// `HMAC_K(data)` (§3.1.1). See [`crate::rfc6979_p256::hmac_k`]'s docs. +/// As [`crate::rfc6979_p256::hmac_k`], including why this returns a [`Secret`] rather than a bare +/// array: see that function's docs and [`generate_k`]'s "What is scrubbed" section. +fn hmac_k(key: &[u8; HLEN], data: &[&[u8]]) -> Secret<[u8; HLEN]> { + let key_material = KeyMaterial::::from_bytes_as_type(key, KeyType::MACKey) + .expect("HLEN-byte key always fits"); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key_material) + .expect("weak key always allowed"); + let mut hmac = hmac_key.create_generator(); + for piece in data { + hmac.do_update(piece); + } + let mut out = Secret::<[u8; HLEN]>::new(); + hmac.do_final_out(&mut *out).expect("HLEN-byte output buffer always fits HMAC-SHA256's output"); + out +} + +/// RFC 6979 §2.3.4 `bits2octets(h1)`. See [`crate::rfc6979_p256::bits2octets`]'s docs -- the same +/// single-subtraction argument applies here since secp256k1's `n` is likewise close enough to +/// `2^256` for a 256-bit input to always be `< 2n`. +fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { + let limbs = p256k1_sec1::limbs_from_be_bytes(h1); + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 4]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + p256k1_sec1::be_bytes_from_limbs(&reduced) +} + +/// `true` iff the raw (unreduced) candidate bytes, read as a big-endian integer, are in `[1, +/// n-1]` -- RFC 6979 step h.3's comparison against `q`, by [`crate::rfc6979_p256::limbs_in_range`]. +fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { + crate::rfc6979_p256::limbs_in_range(&p256k1_sec1::limbs_from_be_bytes(candidate), &N_LIMBS) +} + +/// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and +/// message hash `h1 = H(m)`. See [`crate::rfc6979_p256::generate_k`]'s docs for the rejection-loop +/// constant-time argument, which applies unchanged here; `2^256 - n` is about `2^128`, so a +/// candidate is rejected with probability about `2^-128`. +pub fn generate_k(d: &P256K1Scalar, h1: &[u8; HLEN]) -> P256K1Scalar { + let mut int2octets_d = Secret::<[u8; HLEN]>::new(); + *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] + let bits2octets_h1 = bits2octets(h1); + + // step c + let mut key = Secret::<[u8; HLEN]>::new(); + // step b + let mut v = Secret::<[u8; HLEN]>::new(); + *v = [0x01u8; HLEN]; + + // step d + key = hmac_k(&key, &[&*v, &[0x00], &*int2octets_d, &bits2octets_h1]); + // step e + v = hmac_k(&key, &[&*v]); + // step f + key = hmac_k(&key, &[&*v, &[0x01], &*int2octets_d, &bits2octets_h1]); + // step g + v = hmac_k(&key, &[&*v]); + + // step h + loop { + // step h.1-h.2: T = HMAC_K(V) suffices in one round since tlen == qlen == HLEN*8 (see + // module docs) + v = hmac_k(&key, &[&*v]); + if candidate_in_range(&v) { + // in [1, n-1] already: from_limbs's reduction is a no-op safety net, not a real reduce + return P256K1Scalar::from_limbs(p256k1_sec1::limbs_from_be_bytes(&v)); + } + key = hmac_k(&key, &[&*v, &[0x00]]); + v = hmac_k(&key, &[&*v]); + } +} + +// `candidate_in_range` is private and, through `generate_k`'s rejection loop, only ever reached on +// candidates that -- for any real message/key input -- are essentially always in range, so no +// realistic integration test exercises its reject path; the QUALITY_AND_STYLE.md private-function +// exception applies. Known answers: `0` and `n` are the immediate out-of-range neighbours of the +// valid interval `[1, n-1]`. +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn candidate_in_range_boundary_cases() { + assert!(!candidate_in_range(&[0u8; HLEN])); + + let mut one = [0u8; HLEN]; + one[HLEN - 1] = 1; + assert!(candidate_in_range(&one)); + + let n_bytes = p256k1_sec1::be_bytes_from_limbs(&N_LIMBS); + assert!(!candidate_in_range(&n_bytes)); + + let mut n_minus_1_bytes = n_bytes; + *n_minus_1_bytes.last_mut().unwrap() -= 1; // N_LIMBS[0] is odd, so this never borrows + assert!(candidate_in_range(&n_minus_1_bytes)); + + assert!(!candidate_in_range(&[0xffu8; HLEN])); + } +} diff --git a/crypto/ecdsa/src/rfc6979_p384.rs b/crypto/ecdsa/src/rfc6979_p384.rs new file mode 100644 index 000000000..1d0e40eb2 --- /dev/null +++ b/crypto/ecdsa/src/rfc6979_p384.rs @@ -0,0 +1,104 @@ +//! RFC 6979 §3.2 / FIPS 186-5 Appendix A.3.3: the deterministic per-message secret `k` for P-384, +//! fixed to SHA-384. Identical in shape to [`crate::rfc6979_p256`] -- see that module's docs for the +//! full derivation -- with `HLEN = 48` (SHA-384's output) and P-384's types substituted. `qlen == +//! hlen == 384` here too, so the same shortcuts collapse the same way, guarded by the same kind of +//! compile-time assertion. + +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle_core::traits::MAC; +use bouncycastle_ec::nat; +use bouncycastle_ec::p384_scalar::{N_LIMBS, P384Scalar}; +use bouncycastle_ec::p384_sec1; +use bouncycastle_hmac::HmacKey; +use bouncycastle_sha2::SHA384; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +const HLEN: usize = 48; + +const _: () = + assert!(HLEN * 8 == 384, "int2octets/bits2octets shortcuts assume hlen == qlen == 384"); + +/// As [`crate::rfc6979_p256::hmac_k`], including why this returns a [`Secret`] rather than a bare +/// array: see that function's docs and [`generate_k`]'s "What is scrubbed" section. +fn hmac_k(key: &[u8; HLEN], data: &[&[u8]]) -> Secret<[u8; HLEN]> { + let key_material = KeyMaterial::::from_bytes_as_type(key, KeyType::MACKey) + .expect("HLEN-byte key always fits"); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key_material) + .expect("weak key always allowed"); + let mut hmac = hmac_key.create_generator(); + for piece in data { + hmac.do_update(piece); + } + let mut out = Secret::<[u8; HLEN]>::new(); + hmac.do_final_out(&mut *out).expect("HLEN-byte output buffer always fits HMAC-SHA384's output"); + out +} + +/// RFC 6979 §2.3.4 `bits2octets(h1)`. See [`crate::rfc6979_p256::bits2octets`]'s docs. +fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { + let limbs = p384_sec1::limbs_from_be_bytes(h1); + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 6]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + p384_sec1::be_bytes_from_limbs(&reduced) +} + +/// `true` iff the raw (unreduced) candidate bytes, read as a big-endian integer, are in `[1, +/// n-1]` -- RFC 6979 step h.3's comparison against `q`, by [`crate::rfc6979_p256::limbs_in_range`]. +fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { + crate::rfc6979_p256::limbs_in_range(&p384_sec1::limbs_from_be_bytes(candidate), &N_LIMBS) +} + +/// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and +/// message hash `h1 = H(m)`. See [`crate::rfc6979_p256::generate_k`]'s docs, including the +/// rejection-loop constant-time argument, which applies unchanged here; `n` is within `2^-194` of +/// `2^384`, so a candidate is rejected with probability about `2^-194`. +pub fn generate_k(d: &P384Scalar, h1: &[u8; HLEN]) -> P384Scalar { + let mut int2octets_d = Secret::<[u8; HLEN]>::new(); + *int2octets_d = d.to_be_bytes(); + let bits2octets_h1 = bits2octets(h1); + + let mut key = Secret::<[u8; HLEN]>::new(); + let mut v = Secret::<[u8; HLEN]>::new(); + *v = [0x01u8; HLEN]; + + key = hmac_k(&key, &[&*v, &[0x00], &*int2octets_d, &bits2octets_h1]); + v = hmac_k(&key, &[&*v]); + key = hmac_k(&key, &[&*v, &[0x01], &*int2octets_d, &bits2octets_h1]); + v = hmac_k(&key, &[&*v]); + + loop { + v = hmac_k(&key, &[&*v]); + if candidate_in_range(&v) { + return P384Scalar::from_limbs(p384_sec1::limbs_from_be_bytes(&v)); + } + key = hmac_k(&key, &[&*v, &[0x00]]); + v = hmac_k(&key, &[&*v]); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn candidate_in_range_boundary_cases() { + assert!(!candidate_in_range(&[0u8; HLEN])); + + let mut one = [0u8; HLEN]; + one[HLEN - 1] = 1; + assert!(candidate_in_range(&one)); + + let n_bytes = p384_sec1::be_bytes_from_limbs(&N_LIMBS); + assert!(!candidate_in_range(&n_bytes)); + + let mut n_minus_1_bytes = n_bytes; + *n_minus_1_bytes.last_mut().unwrap() -= 1; + assert!(candidate_in_range(&n_minus_1_bytes)); + + assert!(!candidate_in_range(&[0xffu8; HLEN])); + } +} diff --git a/crypto/ecdsa/src/rfc6979_p521.rs b/crypto/ecdsa/src/rfc6979_p521.rs new file mode 100644 index 000000000..9e99e7d21 --- /dev/null +++ b/crypto/ecdsa/src/rfc6979_p521.rs @@ -0,0 +1,189 @@ +//! RFC 6979 §3.2 / FIPS 186-5 Appendix A.3.3: the deterministic per-message secret `k` for P-521, +//! fixed to SHA-512. Unlike [`crate::rfc6979_p256`] (P-256/SHA-256) and [`crate::rfc6979_p384`] +//! (P-384/SHA-384), `qlen` (521, P-521's order's bit length) and `hlen` (512, SHA-512's output) +//! are **not equal** here, so the shortcuts those modules document (`bits2octets`/`int2octets` +//! needing no truncation, step h.2's loop always running exactly once) do not apply, and this +//! module implements RFC 6979 §3.2 in its general form: +//! +//! - `int2octets(d)`/`bits2octets(H)` still produce `rlen = 8*ceil(qlen/8) = 528` bits (66 bytes, +//! [`RLEN`]) -- one byte wider than `HLEN`'s 64, with the extra byte's low 7 bits always `0` +//! ([`P521Scalar::to_be_bytes`](bouncycastle_ec::p521_scalar::P521Scalar::to_be_bytes) and +//! [`bits2octets`] both produce values already `< n < 2^521`, so their 66-byte big-endian form +//! never sets those bits). +//! - Step h.2's "while `tlen < qlen`" loop runs **twice**: one `HMAC_K(V)` gives only 512 bits, +//! short of the 521 needed, so a second round is required, giving `T` of length 1024 bits. +//! - Step h.3's `bits2int(T)` then keeps the **leftmost 521 bits** of that 1024-bit `T` (RFC 6979 +//! §2.3.2), i.e. the value `T >> 503`: the first 66 bytes of `T` shifted right by the 7 bits +//! by which 528 exceeds 521 -- see [`bits2int_521`] for why a shift, not a mask, is required. + +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle_core::traits::MAC; +use bouncycastle_ec::nat; +use bouncycastle_ec::p521_scalar::{N_LIMBS, P521Scalar}; +use bouncycastle_ec::p521_sec1; +use bouncycastle_hmac::HmacKey; +use bouncycastle_sha2::SHA512; +use bouncycastle_utils::ct; +use bouncycastle_utils::ct::Condition; +use bouncycastle_utils::secret::Secret; + +/// SHA-512's output length in bytes. +const HLEN: usize = 64; + +/// `rlen = 8*ceil(qlen/8)` for P-521's `qlen = 521`: the width `int2octets`/`bits2octets` produce. +const RLEN: usize = 66; + +/// `qlen`, P-521's order's bit length (SP 800-186 §3.2.1.5). +const QLEN: usize = 521; + +// The widths every step of this module is specialised to, pinned the way the other six copies pin +// their `hlen == qlen` shortcut: two `HMAC_K(V)` rounds are exactly enough (`HLEN * 8 < QLEN <= 2 +// * HLEN * 8`), `RLEN` really is `ceil(QLEN / 8)`, and [`bits2int_521`]'s shift is the 7 bits by +// which `RLEN * 8` exceeds `QLEN`. +const _: () = assert!(HLEN * 8 < QLEN && QLEN <= 2 * HLEN * 8, "step h.2 needs exactly two rounds"); +const _: () = assert!(RLEN == QLEN.div_ceil(8), "RLEN must be ceil(qlen / 8)"); +const _: () = assert!(RLEN * 8 - QLEN == 7, "bits2int_521's shift is RLEN * 8 - QLEN"); + +fn hmac_k(key: &[u8; HLEN], data: &[&[u8]]) -> Secret<[u8; HLEN]> { + let key_material = KeyMaterial::::from_bytes_as_type(key, KeyType::MACKey) + .expect("HLEN-byte key always fits"); + let hmac_key = HmacKey::::from_key_material_allow_weak(&key_material) + .expect("weak key always allowed"); + let mut hmac = hmac_key.create_generator(); + for piece in data { + hmac.do_update(piece); + } + let mut out = Secret::<[u8; HLEN]>::new(); + hmac.do_final_out(&mut *out).expect("HLEN-byte output buffer always fits HMAC-SHA512's output"); + out +} + +/// RFC 6979 §2.3.4 `bits2octets(h1)`: `bits2int(h1)` (`h1` is exactly `HLEN` bytes = 512 bits, +/// `< qlen = 521` bits, so no truncation -- it is simply read as a big-endian integer), reduced +/// mod `n` (a single conditional subtraction: the value is `< 2^512 < n`, so this is a no-op in +/// practice, but kept for fidelity to the spec step), then `int2octets` to `RLEN` bytes. +fn bits2octets(h1: &[u8; HLEN]) -> [u8; RLEN] { + let mut padded = [0u8; RLEN]; + padded[RLEN - HLEN..].copy_from_slice(h1); + let limbs = p521_sec1::limbs_from_be_bytes(&padded); + let (diff, borrow) = nat::sub(&limbs, &N_LIMBS); + let mut reduced = [0u64; 9]; + ct::conditional_select(Condition::::from_lsb(borrow), &limbs, &diff, &mut reduced); + p521_sec1::be_bytes_from_limbs(&reduced) +} + +/// RFC 6979 §2.3.2 `bits2int`, specialised to a 1024-bit input (two concatenated `HLEN`-byte `V` +/// values) and `qlen = 521`: keeps the leftmost 521 bits and discards the rest, per the module +/// docs. Returns the result directly as [`RLEN`] big-endian bytes (an integer `< 2^521`, so +/// [`p521_sec1::limbs_from_be_bytes`] on it is exactly `bits2int`'s value, matching +/// [`crate::rfc6979_p256::candidate_in_range`]'s "compared to `n`, not reduced" step h.3 semantics). +/// +/// Keeping "the leftmost 521 bits" as a *value* is not simply truncating the input to its first +/// 66 bytes and masking the last one -- that keeps each surviving bit in its *original* position, +/// which is `2^7` too large (`RLEN*8 - 521 = 7` unused low bits remain). The 66-byte prefix (`v1` +/// followed by `v2`'s first two bytes) must actually be shifted right by those 7 bits, carrying +/// bits across the byte boundary, to land the result in `[0, 2^521)` the way every other `< +/// 2^521` value in this crate is represented. +fn bits2int_521(v1: &[u8; HLEN], v2: &[u8; HLEN]) -> Secret<[u8; RLEN]> { + let mut prefix = [0u8; RLEN]; + prefix[..HLEN].copy_from_slice(v1); + prefix[HLEN] = v2[0]; + prefix[HLEN + 1] = v2[1]; + + let mut shifted = Secret::<[u8; RLEN]>::new(); + shifted[0] = prefix[0] >> 7; + for i in 1..RLEN { + // `prefix[i] >> 7` occupies only bit 0; `prefix[i - 1] << 1` occupies bits 1-7 (its own + // top bit is shifted out) -- disjoint, so `|` and `^` agree here. + shifted[i] = (prefix[i] >> 7) | (prefix[i - 1] << 1); + } + shifted +} + +/// `true` iff the raw (unreduced) candidate bytes, read as a big-endian integer, are in `[1, +/// n-1]` -- RFC 6979 step h.3's comparison against `q`, by [`crate::rfc6979_p256::limbs_in_range`]. +fn candidate_in_range(candidate: &[u8; RLEN]) -> bool { + crate::rfc6979_p256::limbs_in_range(&p521_sec1::limbs_from_be_bytes(candidate), &N_LIMBS) +} + +/// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and +/// message hash `h1 = H(m)`. See [`crate::rfc6979_p256::generate_k`]'s docs; step h differs as +/// described in the module docs (two `HMAC_K(V)` rounds per candidate, then a 521-bit truncation). +/// The rejection-loop constant-time argument there applies unchanged here; `n` is within `2^-262` +/// of `2^521`, so a 521-bit candidate is rejected with probability about `2^-262`. +pub fn generate_k(d: &P521Scalar, h1: &[u8; HLEN]) -> P521Scalar { + let mut int2octets_d = Secret::<[u8; RLEN]>::new(); + *int2octets_d = d.to_be_bytes(); + let bits2octets_h1 = bits2octets(h1); + + let mut key = Secret::<[u8; HLEN]>::new(); + let mut v = Secret::<[u8; HLEN]>::new(); + *v = [0x01u8; HLEN]; + + key = hmac_k(&key, &[&*v, &[0x00], &*int2octets_d, &bits2octets_h1]); + v = hmac_k(&key, &[&*v]); + key = hmac_k(&key, &[&*v, &[0x01], &*int2octets_d, &bits2octets_h1]); + v = hmac_k(&key, &[&*v]); + + loop { + let v1 = hmac_k(&key, &[&*v]); + let v2 = hmac_k(&key, &[&*v1]); + // `candidate` is built before `v2` is moved into `v`; both are `Secret`, so the previous + // `v` is scrubbed by that assignment and `v1`/`v2` when they go out of scope. + let candidate = bits2int_521(&v1, &v2); + v = v2; + if candidate_in_range(&candidate) { + return P521Scalar::from_limbs(p521_sec1::limbs_from_be_bytes(&candidate)); + } + key = hmac_k(&key, &[&*v, &[0x00]]); + v = hmac_k(&key, &[&*v]); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn candidate_in_range_boundary_cases() { + assert!(!candidate_in_range(&[0u8; RLEN])); + + let mut one = [0u8; RLEN]; + one[RLEN - 1] = 1; + assert!(candidate_in_range(&one)); + + let n_bytes = p521_sec1::be_bytes_from_limbs(&N_LIMBS); + assert!(!candidate_in_range(&n_bytes)); + + let mut n_minus_1_bytes = n_bytes; + *n_minus_1_bytes.last_mut().unwrap() -= 1; + assert!(candidate_in_range(&n_minus_1_bytes)); + + assert!(!candidate_in_range(&[0xffu8; RLEN])); + } + + #[test] + fn bits2int_521_all_ones_gives_2_pow_521_minus_1() { + // The leftmost 521 bits of an all-ones 1024-bit string are 521 ones, i.e. 2^521 - 1: as a + // right-aligned 66-byte value, a single set bit in the top byte (0b00000001) followed by + // 65 bytes of 0xff (520 more one-bits). + let v1 = [0xffu8; HLEN]; + let v2 = [0xffu8; HLEN]; + let result = bits2int_521(&v1, &v2); + assert_eq!(result[0], 0x01); + assert_eq!(result[1..], [0xffu8; RLEN - 1]); + } + + #[test] + fn bits2int_521_drops_bit_521_and_beyond() { + // v2's low 7 bits (the last 7 bits of the 1024-bit input, positions 521-527 of the + // leftmost-521-bit window's complement) must never survive into the result: setting only + // v2's very last bit must produce the same output as an all-zero v2. + let v1 = [0u8; HLEN]; + let mut v2 = [0u8; HLEN]; + v2[HLEN - 1] = 0x01; // the least significant bit of the whole 1024-bit input + let result = bits2int_521(&v1, &v2); + assert_eq!(*result, [0u8; RLEN]); + } +} diff --git a/crypto/ecdsa/tests/cavp_common/mod.rs b/crypto/ecdsa/tests/cavp_common/mod.rs new file mode 100644 index 000000000..4985fb366 --- /dev/null +++ b/crypto/ecdsa/tests/cavp_common/mod.rs @@ -0,0 +1,199 @@ +//! Shared parser for the legacy (pre-ACVP, 2011-era) NIST CAVP `.rsp` text format used by +//! `ECDSA_SigVer.rsp`, `ECDSA_PKV.rsp`, `ECDSA_KeyPair.rsp` and `ECDSA_SigGen.txt` in +//! `bc-test-data/crypto/cavp/` -- distinct from the newer JSON ACVP format the +//! `aes`/`modes`/`mldsa`/`mlkem` crates' own `bc-test-data.rs` files parse. Each test binary that +//! includes this module uses a subset of it. +//! +//! Unlike those other suites, a missing `bc-test-data` checkout is a test **failure** here, not a +//! warning and a vacuous pass: these files are the only external check on this crate's signing +//! and verification against NIST's own values, and a green run that never read them would be +//! indistinguishable from one that did. Clone `bc-test-data` next to this repository (and symlink +//! it at `/tmp/bc-test-data` for `cargo mutants`, whose copied tree resolves the relative path +//! there -- see `CLAUDE.md`). + +#![allow(dead_code)] + +use bouncycastle_core::errors::RNGError; +use bouncycastle_core::key_material::KeyMaterialTrait; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::RNG; +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/crypto/cavp"; +const TEST_DATA_PATH: &str = "../bc-test-data/crypto/cavp"; + +/// Returns the contents of `filename` from `bc-test-data/crypto/cavp/`. Panics, failing the test, +/// if the repo is not checked out or the file is missing -- see the module docs for why this suite +/// does not skip. +pub fn get_test_data(filename: &str) -> String { + let dir = [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] + .into_iter() + .find(|d| Path::new(d).exists()) + .unwrap_or_else(|| { + panic!( + "bc-test-data not found (looked in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); the ECDSA CAVP suites require it rather than skipping" + ) + }); + fs::read_to_string(format!("{dir}/{filename}")) + .unwrap_or_else(|e| panic!("failed to read CAVP vector file {dir}/{filename}: {e}")) +} + +/// An [`RNG`] that hands out one fixed big-endian value, zero-padded on the left to whatever width +/// is asked for. This is how `ECDSA_SigGen.txt`'s per-message secret `k` is injected: every +/// curve's `sign_rng` draws its DRBG bytes and reduces them per FIPS 186-5 Appendix A.4.1 +/// (`x mod (n-1)`, then `+ 1`), so an RNG returning `k - 1` -- which is `< n - 1`, making the +/// reduction the identity -- produces exactly `k`, through the public API and with no test-only +/// hook in the crate. Only `next_bytes_out`/`next_bytes` are meaningful; nothing in the signing +/// path calls the rest. +pub struct FixedBytesRng { + value_be: Vec, +} + +impl FixedBytesRng { + pub fn new(value_be: Vec) -> Self { + Self { value_be } + } +} + +impl RNG for FixedBytesRng { + fn add_seed_keymaterial(&mut self, _seed: &dyn KeyMaterialTrait) -> Result<(), RNGError> { + Ok(()) + } + + fn next_int(&mut self) -> Result { + unimplemented!("FixedBytesRng only serves next_bytes_out") + } + + fn next_bytes(&mut self, len: usize) -> Result, RNGError> { + let mut out = vec![0u8; len]; + self.next_bytes_out(&mut out)?; + Ok(out) + } + + fn next_bytes_out(&mut self, out: &mut [u8]) -> Result { + assert!( + out.len() >= self.value_be.len(), + "requested {} bytes but the fixed value is {} bytes wide", + out.len(), + self.value_be.len() + ); + let pad = out.len() - self.value_be.len(); + out[..pad].fill(0); + out[pad..].copy_from_slice(&self.value_be); + Ok(out.len()) + } + + fn fill_keymaterial_out(&mut self, _out: &mut dyn KeyMaterialTrait) -> Result { + unimplemented!("FixedBytesRng only serves next_bytes_out") + } + + fn security_strength(&self) -> SecurityStrength { + SecurityStrength::_256bit + } +} + +/// Big-endian `value - 1` in place. Only ever called on a CAVP `k`, which is in `[1, n-1]` and so +/// never zero; panics rather than wrap if it is. +pub fn decrement_be(value: &mut [u8]) { + for byte in value.iter_mut().rev() { + if *byte > 0 { + *byte -= 1; + return; + } + *byte = 0xff; + } + panic!("decrement_be called on zero"); +} + +/// One `Key = value`-line record, plus the `[...]` section header it fell under. +pub struct Record { + pub section: String, + pub fields: BTreeMap, +} + +fn kv(line: &str) -> Option<(&str, &str)> { + let (k, v) = line.split_once('=')?; + Some((k.trim(), v.trim())) +} + +/// Parses every `Key = value` record in `content`, grouped by the most recent `[Section]` header. +/// A record ends at the next blank line. Only bracket lines shaped like a curve name (`P-256`, +/// `K-163`, `B-233`, ...) update the section: `ECDSA_KeyPair.rsp` nests a second, descriptive +/// bracket line (`[B.4.2 Key Pair Generation by Testing Candidates]`) under each curve's, which +/// this deliberately ignores rather than losing the curve name. That file also has a lone `N = 10` +/// line before its first real record; callers filter records by the fields they actually need +/// (e.g. requiring `d`) rather than this parser guessing which stray lines are noise. +pub fn parse_records(content: &str) -> Vec { + let mut records = Vec::new(); + let mut section = String::new(); + let mut current = BTreeMap::new(); + + let flush = |current: &mut BTreeMap, + records: &mut Vec, + section: &str| { + if !current.is_empty() { + records.push(Record { section: section.to_string(), fields: std::mem::take(current) }); + } + }; + + for raw_line in content.lines() { + let line = raw_line.trim(); + if line.is_empty() { + flush(&mut current, &mut records, §ion); + continue; + } + if line.starts_with('#') { + continue; + } + if let Some(inner) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { + if inner.starts_with("P-") || inner.starts_with("K-") || inner.starts_with("B-") { + flush(&mut current, &mut records, §ion); + section = inner.to_string(); + } + continue; + } + if let Some((k, v)) = kv(line) { + current.insert(k.to_string(), v.to_string()); + } + } + flush(&mut current, &mut records, §ion); + records +} + +/// Decodes a CAVP hex field into big-endian bytes, left-padded with zero bytes to at least +/// `width` (and, if needed, a single leading zero nibble to make the hex string even-length -- +/// CAVP sometimes omits the leading zero nibble of the most significant byte, e.g. for P-521 +/// values whose top byte only ever has 1 significant bit). +/// +/// Deliberately does *not* reject or truncate a value wider than `width`: `ECDSA_PKV.rsp`'s +/// negative "Q_x or Q_y out of range" vectors are exactly that -- a coordinate one or more bytes +/// too wide to be a valid field element -- and the point of those vectors is to check that the +/// public-key decoder built from this value rejects it, which an oversized SEC 1 encoding does +/// naturally (wrong total length) without this helper needing to special-case the reason. +pub fn hex_field(fields: &BTreeMap, key: &str, width: usize) -> Vec { + let hex = fields.get(key).unwrap_or_else(|| panic!("record missing field {key}")); + let hex = if hex.len() % 2 == 1 { format!("0{hex}") } else { hex.clone() }; + let bytes = + bouncycastle_hex::decode(&hex).unwrap_or_else(|e| panic!("bad hex in {key}: {e:?}")); + if bytes.len() >= width { + return bytes; + } + let mut out = vec![0u8; width]; + out[width - bytes.len()..].copy_from_slice(&bytes); + out +} + +/// Decodes a CAVP hex field of whatever length it happens to be (e.g. `Msg`, which is the message +/// itself, not a fixed-width field element). +pub fn hex_bytes(fields: &BTreeMap, key: &str) -> Vec { + let hex = fields.get(key).unwrap_or_else(|| panic!("record missing field {key}")); + bouncycastle_hex::decode(hex).unwrap_or_else(|e| panic!("bad hex in {key}: {e:?}")) +} + +/// `true` iff a CAVP `Result = P (0 )` / `Result = F (...)` field indicates success. +pub fn result_is_pass(fields: &BTreeMap) -> bool { + fields.get("Result").unwrap_or_else(|| panic!("record missing Result field")).starts_with('P') +} diff --git a/crypto/ecdsa/tests/cavp_p256.rs b/crypto/ecdsa/tests/cavp_p256.rs new file mode 100644 index 000000000..402772ee0 --- /dev/null +++ b/crypto/ecdsa/tests/cavp_p256.rs @@ -0,0 +1,166 @@ +//! NIST CAVP legacy vectors for P-256/SHA-256, from `bc-test-data/crypto/cavp/`: `ECDSA_SigVer.rsp` +//! (signature verification, positive and negative), `ECDSA_PKV.rsp` (public key validation), +//! `ECDSA_KeyPair.rsp` (private/public key-pair consistency), and `ECDSA_SigGen.txt` (signature +//! generation with the per-message secret `k` given, so signing output itself is checked -- see +//! `cavp_siggen`). `ECDSA_SigGen.rsp`, the response-file form of the same vectors without `d` or +//! `k`, is not read: as verify-only data it duplicates `SigVer`'s `Result = P` cases. +//! +//! See `cavp_common`'s docs for the parser, and for why a missing `bc-test-data` checkout fails +//! these tests rather than skipping them. `K-*`/`B-*` (binary-field) curve sections in these +//! files are not read: this crate implements prime-field curves only. +//! +//! `ECDSA_KeyPair.rsp` gives `(d, Qx, Qy)` triples with no direct "derive the public key from a +//! private key" entry point in this crate's public API to check `[d]G == Q` against. Rather than +//! reach into `bouncycastle_ec` directly (which would test that crate's multiplier, not anything +//! `bouncycastle-ecdsa` itself does), each triple is checked through the public API instead: sign a +//! fixed message with the private key built from `d`, then verify that signature against the +//! public key built from `(Qx, Qy)`. Verification's own math (`R' = [u]G + [v]Q`) only succeeds if +//! `Q` really is `[d]G`, so this is a full, if indirect, consistency check. + +mod cavp_common; + +use bouncycastle_core::signature_key::{SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ecdsa::ecdsa_p256::SIG_LEN; +use bouncycastle_ecdsa::keys_p256::{ECDSAP256PrivateKey, ECDSAP256PublicKey}; +use bouncycastle_rng::DefaultRNG; +use cavp_common::{ + FixedBytesRng, Record, decrement_be, get_test_data, hex_bytes, hex_field, parse_records, + result_is_pass, +}; + +const FIELD_WIDTH: usize = 32; +const SECTION: &str = "P-256,SHA-256"; +const KEY_SECTION: &str = "P-256"; + +/// SEC 1 §2.3.3 case 3 (`04 || X || Y`), built dynamically rather than into a fixed-size array: +/// `ECDSA_PKV.rsp`'s "out of range" vectors deliberately supply a coordinate wider than +/// [`FIELD_WIDTH`] (see `hex_field`'s docs), and this must reproduce that oversized encoding +/// rather than truncate it, so the decoder's own length check is what rejects it. +fn uncompressed_pk(qx: &[u8], qy: &[u8]) -> Vec { + let mut out = vec![0x04u8]; + out.extend_from_slice(qx); + out.extend_from_slice(qy); + out +} + +fn signature_bytes(r: &[u8], s: &[u8]) -> [u8; SIG_LEN] { + let mut out = [0u8; SIG_LEN]; + out[..FIELD_WIDTH].copy_from_slice(r); + out[FIELD_WIDTH..].copy_from_slice(s); + out +} + +#[test] +fn cavp_sigver() { + let content = get_test_data("ECDSA_SigVer.rsp"); + let records: Vec = parse_records(&content) + .into_iter() + .filter(|r| r.section == SECTION && r.fields.contains_key("Result")) + .collect(); + assert!(!records.is_empty(), "no P-256,SHA-256 SigVer records found"); + + for record in &records { + let msg = hex_bytes(&record.fields, "Msg"); + let qx = hex_field(&record.fields, "Qx", FIELD_WIDTH); + let qy = hex_field(&record.fields, "Qy", FIELD_WIDTH); + let r = hex_field(&record.fields, "R", FIELD_WIDTH); + let s = hex_field(&record.fields, "S", FIELD_WIDTH); + let expect_pass = result_is_pass(&record.fields); + + // A malformed/out-of-range public key is itself grounds for the CAVP vector to be a + // failure case; from_bytes rejecting it satisfies that without ever reaching verify. + let pk = match ECDSAP256PublicKey::from_bytes(&uncompressed_pk(&qx, &qy)) { + Ok(pk) => pk, + Err(_) => { + assert!(!expect_pass, "public key was rejected but this vector expects Result = P"); + continue; + } + }; + let sig = signature_bytes(&r, &s); + let result = pk.verify(&msg, None, &sig); + assert_eq!(result.is_ok(), expect_pass, "mismatch for record {:?}", record.fields); + } +} + +#[test] +fn cavp_pkv() { + let content = get_test_data("ECDSA_PKV.rsp"); + let records: Vec = parse_records(&content) + .into_iter() + .filter(|r| r.section == KEY_SECTION && r.fields.contains_key("Result")) + .collect(); + assert!(!records.is_empty(), "no P-256 PKV records found"); + + for record in &records { + let qx = hex_field(&record.fields, "Qx", FIELD_WIDTH); + let qy = hex_field(&record.fields, "Qy", FIELD_WIDTH); + let expect_pass = result_is_pass(&record.fields); + + let result = ECDSAP256PublicKey::from_bytes(&uncompressed_pk(&qx, &qy)); + assert_eq!(result.is_ok(), expect_pass, "mismatch for record {:?}", record.fields); + } +} + +#[test] +fn cavp_keypair_consistency() { + let content = get_test_data("ECDSA_KeyPair.rsp"); + let records: Vec = parse_records(&content) + .into_iter() + .filter(|r| r.section == KEY_SECTION && r.fields.contains_key("d")) + .collect(); + assert!(!records.is_empty(), "no P-256 KeyPair records found"); + + let msg = b"CAVP KeyPair (d, Q) consistency check"; + for record in &records { + let d = hex_field(&record.fields, "d", FIELD_WIDTH); + let qx = hex_field(&record.fields, "Qx", FIELD_WIDTH); + let qy = hex_field(&record.fields, "Qy", FIELD_WIDTH); + + let sk = ECDSAP256PrivateKey::from_bytes(&d).expect("CAVP d must be a valid private key"); + let pk = ECDSAP256PublicKey::from_bytes(&uncompressed_pk(&qx, &qy)) + .expect("CAVP Q must be a valid public key"); + + let sig = sk + .sign(&mut DefaultRNG::default(), msg, None) + .expect("signing with a valid key must succeed"); + pk.verify(msg, None, &sig).expect("Q must be [d]G for this (d, Q) pair to verify"); + } +} + +/// `ECDSA_SigGen.txt` -- NIST's `SigGen.txt` from `186-4ecdsatestvectors.zip`, the form of the +/// SigGen vectors that carries `d` and the per-message secret `k`, which the response-file form +/// `ECDSA_SigGen.rsp` omits. `k` is injected through [`FixedBytesRng`] and `sign_rng` +/// (see that type's docs for why an RNG returning `k - 1` yields exactly `k`), so this pins +/// signing output against NIST's values through the public API. RFC 6979's own vectors pin the +/// deterministic path; this is the only NIST check on the randomised one. +#[test] +fn cavp_siggen() { + let content = get_test_data("ECDSA_SigGen.txt"); + let records: Vec = parse_records(&content) + .into_iter() + .filter(|r| r.section == SECTION && r.fields.contains_key("k")) + .collect(); + assert!(!records.is_empty(), "no P-256 SigGen records with k found"); + + for record in &records { + let msg = hex_bytes(&record.fields, "Msg"); + let d = hex_field(&record.fields, "d", FIELD_WIDTH); + let qx = hex_field(&record.fields, "Qx", FIELD_WIDTH); + let qy = hex_field(&record.fields, "Qy", FIELD_WIDTH); + let mut k_minus_1 = hex_field(&record.fields, "k", FIELD_WIDTH); + decrement_be(&mut k_minus_1); + let r = hex_field(&record.fields, "R", FIELD_WIDTH); + let s = hex_field(&record.fields, "S", FIELD_WIDTH); + + let sk = ECDSAP256PrivateKey::from_bytes(&d).expect("CAVP d must be a valid private key"); + let pk = ECDSAP256PublicKey::from_bytes(&uncompressed_pk(&qx, &qy)) + .expect("CAVP Q must be a valid public key"); + + let mut rng = FixedBytesRng::new(k_minus_1); + let sig = + sk.sign(&mut rng, &msg, None).expect("signing with a valid key and k must succeed"); + assert_eq!(sig, signature_bytes(&r, &s), "mismatch for record {:?}", record.fields); + pk.verify(&msg, None, &sig).expect("NIST's own signature must verify"); + } +} diff --git a/crypto/ecdsa/tests/cavp_p384.rs b/crypto/ecdsa/tests/cavp_p384.rs new file mode 100644 index 000000000..11bcb2873 --- /dev/null +++ b/crypto/ecdsa/tests/cavp_p384.rs @@ -0,0 +1,151 @@ +//! NIST CAVP legacy vectors for P-384/SHA-384, from `bc-test-data/crypto/cavp/`. Identical in shape +//! to `cavp_p256.rs` -- see that file's docs for the full reasoning (how `ECDSA_SigGen.txt`'s `k` +//! is injected, and how `ECDSA_KeyPair.rsp`'s `(d, Q)` pairs are checked through the public API +//! rather than by reaching into `bouncycastle_ec` directly) -- with P-384's types and field width +//! substituted. + +mod cavp_common; + +use bouncycastle_core::signature_key::{SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ecdsa::ecdsa_p384::SIG_LEN; +use bouncycastle_ecdsa::keys_p384::{ECDSAP384PrivateKey, ECDSAP384PublicKey}; +use bouncycastle_rng::DefaultRNG; +use cavp_common::{ + FixedBytesRng, Record, decrement_be, get_test_data, hex_bytes, hex_field, parse_records, + result_is_pass, +}; + +const FIELD_WIDTH: usize = 48; +const SECTION: &str = "P-384,SHA-384"; +const KEY_SECTION: &str = "P-384"; + +/// SEC 1 §2.3.3 case 3 (`04 || X || Y`), built dynamically rather than into a fixed-size array: +/// `ECDSA_PKV.rsp`'s "out of range" vectors deliberately supply a coordinate wider than +/// [`FIELD_WIDTH`] (see `hex_field`'s docs), and this must reproduce that oversized encoding +/// rather than truncate it, so the decoder's own length check is what rejects it. +fn uncompressed_pk(qx: &[u8], qy: &[u8]) -> Vec { + let mut out = vec![0x04u8]; + out.extend_from_slice(qx); + out.extend_from_slice(qy); + out +} + +fn signature_bytes(r: &[u8], s: &[u8]) -> [u8; SIG_LEN] { + let mut out = [0u8; SIG_LEN]; + out[..FIELD_WIDTH].copy_from_slice(r); + out[FIELD_WIDTH..].copy_from_slice(s); + out +} + +#[test] +fn cavp_sigver() { + let content = get_test_data("ECDSA_SigVer.rsp"); + let records: Vec = parse_records(&content) + .into_iter() + .filter(|r| r.section == SECTION && r.fields.contains_key("Result")) + .collect(); + assert!(!records.is_empty(), "no P-384,SHA-384 SigVer records found"); + + for record in &records { + let msg = hex_bytes(&record.fields, "Msg"); + let qx = hex_field(&record.fields, "Qx", FIELD_WIDTH); + let qy = hex_field(&record.fields, "Qy", FIELD_WIDTH); + let r = hex_field(&record.fields, "R", FIELD_WIDTH); + let s = hex_field(&record.fields, "S", FIELD_WIDTH); + let expect_pass = result_is_pass(&record.fields); + + let pk = match ECDSAP384PublicKey::from_bytes(&uncompressed_pk(&qx, &qy)) { + Ok(pk) => pk, + Err(_) => { + assert!(!expect_pass, "public key was rejected but this vector expects Result = P"); + continue; + } + }; + let sig = signature_bytes(&r, &s); + let result = pk.verify(&msg, None, &sig); + assert_eq!(result.is_ok(), expect_pass, "mismatch for record {:?}", record.fields); + } +} + +#[test] +fn cavp_pkv() { + let content = get_test_data("ECDSA_PKV.rsp"); + let records: Vec = parse_records(&content) + .into_iter() + .filter(|r| r.section == KEY_SECTION && r.fields.contains_key("Result")) + .collect(); + assert!(!records.is_empty(), "no P-384 PKV records found"); + + for record in &records { + let qx = hex_field(&record.fields, "Qx", FIELD_WIDTH); + let qy = hex_field(&record.fields, "Qy", FIELD_WIDTH); + let expect_pass = result_is_pass(&record.fields); + + let result = ECDSAP384PublicKey::from_bytes(&uncompressed_pk(&qx, &qy)); + assert_eq!(result.is_ok(), expect_pass, "mismatch for record {:?}", record.fields); + } +} + +#[test] +fn cavp_keypair_consistency() { + let content = get_test_data("ECDSA_KeyPair.rsp"); + let records: Vec = parse_records(&content) + .into_iter() + .filter(|r| r.section == KEY_SECTION && r.fields.contains_key("d")) + .collect(); + assert!(!records.is_empty(), "no P-384 KeyPair records found"); + + let msg = b"CAVP KeyPair (d, Q) consistency check"; + for record in &records { + let d = hex_field(&record.fields, "d", FIELD_WIDTH); + let qx = hex_field(&record.fields, "Qx", FIELD_WIDTH); + let qy = hex_field(&record.fields, "Qy", FIELD_WIDTH); + + let sk = ECDSAP384PrivateKey::from_bytes(&d).expect("CAVP d must be a valid private key"); + let pk = ECDSAP384PublicKey::from_bytes(&uncompressed_pk(&qx, &qy)) + .expect("CAVP Q must be a valid public key"); + + let sig = sk + .sign(&mut DefaultRNG::default(), msg, None) + .expect("signing with a valid key must succeed"); + pk.verify(msg, None, &sig).expect("Q must be [d]G for this (d, Q) pair to verify"); + } +} + +/// `ECDSA_SigGen.txt` -- NIST's `SigGen.txt` from `186-4ecdsatestvectors.zip`, the form of the +/// SigGen vectors that carries `d` and the per-message secret `k`, which the response-file form +/// `ECDSA_SigGen.rsp` omits. `k` is injected through [`FixedBytesRng`] and `sign_rng` +/// (see that type's docs for why an RNG returning `k - 1` yields exactly `k`), so this pins +/// signing output against NIST's values through the public API. RFC 6979's own vectors pin the +/// deterministic path; this is the only NIST check on the randomised one. +#[test] +fn cavp_siggen() { + let content = get_test_data("ECDSA_SigGen.txt"); + let records: Vec = parse_records(&content) + .into_iter() + .filter(|r| r.section == SECTION && r.fields.contains_key("k")) + .collect(); + assert!(!records.is_empty(), "no P-384 SigGen records with k found"); + + for record in &records { + let msg = hex_bytes(&record.fields, "Msg"); + let d = hex_field(&record.fields, "d", FIELD_WIDTH); + let qx = hex_field(&record.fields, "Qx", FIELD_WIDTH); + let qy = hex_field(&record.fields, "Qy", FIELD_WIDTH); + let mut k_minus_1 = hex_field(&record.fields, "k", FIELD_WIDTH); + decrement_be(&mut k_minus_1); + let r = hex_field(&record.fields, "R", FIELD_WIDTH); + let s = hex_field(&record.fields, "S", FIELD_WIDTH); + + let sk = ECDSAP384PrivateKey::from_bytes(&d).expect("CAVP d must be a valid private key"); + let pk = ECDSAP384PublicKey::from_bytes(&uncompressed_pk(&qx, &qy)) + .expect("CAVP Q must be a valid public key"); + + let mut rng = FixedBytesRng::new(k_minus_1); + let sig = + sk.sign(&mut rng, &msg, None).expect("signing with a valid key and k must succeed"); + assert_eq!(sig, signature_bytes(&r, &s), "mismatch for record {:?}", record.fields); + pk.verify(&msg, None, &sig).expect("NIST's own signature must verify"); + } +} diff --git a/crypto/ecdsa/tests/cavp_p521.rs b/crypto/ecdsa/tests/cavp_p521.rs new file mode 100644 index 000000000..12898fc00 --- /dev/null +++ b/crypto/ecdsa/tests/cavp_p521.rs @@ -0,0 +1,152 @@ +//! NIST CAVP legacy vectors for P-521/SHA-512, from `bc-test-data/crypto/cavp/`. Identical in shape +//! to `cavp_p256.rs` -- see that file's docs for the full reasoning (how `ECDSA_SigGen.txt`'s `k` +//! is injected, and how `ECDSA_KeyPair.rsp`'s `(d, Q)` pairs are checked through the public API +//! rather than by reaching into `bouncycastle_ec` directly) -- with P-521's types and field width +//! substituted. `FIELD_WIDTH = 66` is `ceil(521 / 8)`: `hex_field`'s odd-hex-digit padding matters +//! most here, since P-521's top byte only ever has 1 significant bit. + +mod cavp_common; + +use bouncycastle_core::signature_key::{SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ecdsa::ecdsa_p521::SIG_LEN; +use bouncycastle_ecdsa::keys_p521::{ECDSAP521PrivateKey, ECDSAP521PublicKey}; +use bouncycastle_rng::DefaultRNG; +use cavp_common::{ + FixedBytesRng, Record, decrement_be, get_test_data, hex_bytes, hex_field, parse_records, + result_is_pass, +}; + +const FIELD_WIDTH: usize = 66; +const SECTION: &str = "P-521,SHA-512"; +const KEY_SECTION: &str = "P-521"; + +/// SEC 1 §2.3.3 case 3 (`04 || X || Y`), built dynamically rather than into a fixed-size array: +/// `ECDSA_PKV.rsp`'s "out of range" vectors deliberately supply a coordinate wider than +/// [`FIELD_WIDTH`] (see `hex_field`'s docs), and this must reproduce that oversized encoding +/// rather than truncate it, so the decoder's own length check is what rejects it. +fn uncompressed_pk(qx: &[u8], qy: &[u8]) -> Vec { + let mut out = vec![0x04u8]; + out.extend_from_slice(qx); + out.extend_from_slice(qy); + out +} + +fn signature_bytes(r: &[u8], s: &[u8]) -> [u8; SIG_LEN] { + let mut out = [0u8; SIG_LEN]; + out[..FIELD_WIDTH].copy_from_slice(r); + out[FIELD_WIDTH..].copy_from_slice(s); + out +} + +#[test] +fn cavp_sigver() { + let content = get_test_data("ECDSA_SigVer.rsp"); + let records: Vec = parse_records(&content) + .into_iter() + .filter(|r| r.section == SECTION && r.fields.contains_key("Result")) + .collect(); + assert!(!records.is_empty(), "no P-521,SHA-512 SigVer records found"); + + for record in &records { + let msg = hex_bytes(&record.fields, "Msg"); + let qx = hex_field(&record.fields, "Qx", FIELD_WIDTH); + let qy = hex_field(&record.fields, "Qy", FIELD_WIDTH); + let r = hex_field(&record.fields, "R", FIELD_WIDTH); + let s = hex_field(&record.fields, "S", FIELD_WIDTH); + let expect_pass = result_is_pass(&record.fields); + + let pk = match ECDSAP521PublicKey::from_bytes(&uncompressed_pk(&qx, &qy)) { + Ok(pk) => pk, + Err(_) => { + assert!(!expect_pass, "public key was rejected but this vector expects Result = P"); + continue; + } + }; + let sig = signature_bytes(&r, &s); + let result = pk.verify(&msg, None, &sig); + assert_eq!(result.is_ok(), expect_pass, "mismatch for record {:?}", record.fields); + } +} + +#[test] +fn cavp_pkv() { + let content = get_test_data("ECDSA_PKV.rsp"); + let records: Vec = parse_records(&content) + .into_iter() + .filter(|r| r.section == KEY_SECTION && r.fields.contains_key("Result")) + .collect(); + assert!(!records.is_empty(), "no P-521 PKV records found"); + + for record in &records { + let qx = hex_field(&record.fields, "Qx", FIELD_WIDTH); + let qy = hex_field(&record.fields, "Qy", FIELD_WIDTH); + let expect_pass = result_is_pass(&record.fields); + + let result = ECDSAP521PublicKey::from_bytes(&uncompressed_pk(&qx, &qy)); + assert_eq!(result.is_ok(), expect_pass, "mismatch for record {:?}", record.fields); + } +} + +#[test] +fn cavp_keypair_consistency() { + let content = get_test_data("ECDSA_KeyPair.rsp"); + let records: Vec = parse_records(&content) + .into_iter() + .filter(|r| r.section == KEY_SECTION && r.fields.contains_key("d")) + .collect(); + assert!(!records.is_empty(), "no P-521 KeyPair records found"); + + let msg = b"CAVP KeyPair (d, Q) consistency check"; + for record in &records { + let d = hex_field(&record.fields, "d", FIELD_WIDTH); + let qx = hex_field(&record.fields, "Qx", FIELD_WIDTH); + let qy = hex_field(&record.fields, "Qy", FIELD_WIDTH); + + let sk = ECDSAP521PrivateKey::from_bytes(&d).expect("CAVP d must be a valid private key"); + let pk = ECDSAP521PublicKey::from_bytes(&uncompressed_pk(&qx, &qy)) + .expect("CAVP Q must be a valid public key"); + + let sig = sk + .sign(&mut DefaultRNG::default(), msg, None) + .expect("signing with a valid key must succeed"); + pk.verify(msg, None, &sig).expect("Q must be [d]G for this (d, Q) pair to verify"); + } +} + +/// `ECDSA_SigGen.txt` -- NIST's `SigGen.txt` from `186-4ecdsatestvectors.zip`, the form of the +/// SigGen vectors that carries `d` and the per-message secret `k`, which the response-file form +/// `ECDSA_SigGen.rsp` omits. `k` is injected through [`FixedBytesRng`] and `sign_rng` +/// (see that type's docs for why an RNG returning `k - 1` yields exactly `k`), so this pins +/// signing output against NIST's values through the public API. RFC 6979's own vectors pin the +/// deterministic path; this is the only NIST check on the randomised one. +#[test] +fn cavp_siggen() { + let content = get_test_data("ECDSA_SigGen.txt"); + let records: Vec = parse_records(&content) + .into_iter() + .filter(|r| r.section == SECTION && r.fields.contains_key("k")) + .collect(); + assert!(!records.is_empty(), "no P-521 SigGen records with k found"); + + for record in &records { + let msg = hex_bytes(&record.fields, "Msg"); + let d = hex_field(&record.fields, "d", FIELD_WIDTH); + let qx = hex_field(&record.fields, "Qx", FIELD_WIDTH); + let qy = hex_field(&record.fields, "Qy", FIELD_WIDTH); + let mut k_minus_1 = hex_field(&record.fields, "k", FIELD_WIDTH); + decrement_be(&mut k_minus_1); + let r = hex_field(&record.fields, "R", FIELD_WIDTH); + let s = hex_field(&record.fields, "S", FIELD_WIDTH); + + let sk = ECDSAP521PrivateKey::from_bytes(&d).expect("CAVP d must be a valid private key"); + let pk = ECDSAP521PublicKey::from_bytes(&uncompressed_pk(&qx, &qy)) + .expect("CAVP Q must be a valid public key"); + + let mut rng = FixedBytesRng::new(k_minus_1); + let sig = + sk.sign(&mut rng, &msg, None).expect("signing with a valid key and k must succeed"); + assert_eq!(sig, signature_bytes(&r, &s), "mismatch for record {:?}", record.fields); + pk.verify(&msg, None, &sig).expect("NIST's own signature must verify"); + } +} diff --git a/crypto/ecdsa/tests/der_tests.rs b/crypto/ecdsa/tests/der_tests.rs new file mode 100644 index 000000000..d1322e6c6 --- /dev/null +++ b/crypto/ecdsa/tests/der_tests.rs @@ -0,0 +1,385 @@ +//! Known-answer and malformed-input tests for [`bouncycastle_ecdsa::der`]. +//! +//! The known-answer `der` bytes below were computed independently in Python (not from this crate), +//! reproducing ITU-T X.690 §8.3/§10.1's rules directly: strip redundant leading zero bytes from +//! each magnitude, prepend a `0x00` pad byte if the remaining leading byte's high bit is set, then +//! wrap both `INTEGER` TLVs in a `SEQUENCE` TLV, using the short-form length under 128 and the +//! one-octet long form (`0x81 `) at or above it. `random.seed(20260915)`. + +use bouncycastle_ecdsa::der::{decode, encode, max_len}; + +fn round_trip(r: &[u8], s: &[u8], expected_der: &[u8]) { + let mut out = vec![0u8; max_len(r.len())]; + let len = encode(r, s, &mut out); + assert_eq!(&out[..len], expected_der, "encode mismatch for r={r:x?} s={s:x?}"); + + let mut r_out = vec![0u8; r.len()]; + let mut s_out = vec![0u8; s.len()]; + decode(&out[..len], &mut r_out, &mut s_out).expect("decode must accept its own encoding"); + assert_eq!(r_out, r, "decoded r mismatch"); + assert_eq!(s_out, s, "decoded s mismatch"); +} + +#[test] +fn known_answer_p256_width_with_high_bit_leading_r() { + let r: [u8; 32] = [ + 0xb5, 0x69, 0x34, 0xaa, 0x08, 0xce, 0x96, 0x42, 0xe5, 0x51, 0x14, 0x1f, 0x3a, 0x4e, 0x63, + 0x80, 0x06, 0x8e, 0xfd, 0xe9, 0x54, 0xfb, 0x3d, 0xd9, 0x9b, 0x68, 0xa0, 0x81, 0x44, 0x98, + 0x38, 0x9e, + ]; + let s: [u8; 32] = [ + 0xee, 0x4c, 0x9a, 0x49, 0x52, 0x05, 0xaa, 0x3f, 0x4c, 0xaa, 0x6b, 0x21, 0x7b, 0x55, 0x4c, + 0x63, 0x9b, 0xfc, 0xbc, 0xd0, 0x8c, 0x0e, 0x9a, 0xca, 0x1d, 0x3b, 0x0d, 0x6c, 0x33, 0x66, + 0xba, 0x1b, + ]; + let der: [u8; 72] = [ + 0x30, 0x46, 0x02, 0x21, 0x00, 0xb5, 0x69, 0x34, 0xaa, 0x08, 0xce, 0x96, 0x42, 0xe5, 0x51, + 0x14, 0x1f, 0x3a, 0x4e, 0x63, 0x80, 0x06, 0x8e, 0xfd, 0xe9, 0x54, 0xfb, 0x3d, 0xd9, 0x9b, + 0x68, 0xa0, 0x81, 0x44, 0x98, 0x38, 0x9e, 0x02, 0x21, 0x00, 0xee, 0x4c, 0x9a, 0x49, 0x52, + 0x05, 0xaa, 0x3f, 0x4c, 0xaa, 0x6b, 0x21, 0x7b, 0x55, 0x4c, 0x63, 0x9b, 0xfc, 0xbc, 0xd0, + 0x8c, 0x0e, 0x9a, 0xca, 0x1d, 0x3b, 0x0d, 0x6c, 0x33, 0x66, 0xba, 0x1b, + ]; + assert_eq!(max_len(32), 72); + round_trip(&r, &s, &der); +} + +#[test] +fn known_answer_p384_width() { + let r: [u8; 48] = [ + 0xda, 0xe6, 0x75, 0x8d, 0xde, 0xf4, 0xca, 0x42, 0x50, 0x9e, 0x3e, 0x99, 0x1b, 0x7b, 0x5d, + 0xde, 0x45, 0x33, 0x51, 0x75, 0x7a, 0xda, 0x92, 0xab, 0xff, 0x46, 0x9a, 0x00, 0x38, 0x12, + 0x9a, 0x83, 0x07, 0xcb, 0x61, 0x2c, 0xd9, 0x0e, 0xa2, 0xbf, 0xbe, 0x91, 0xf3, 0xa4, 0x01, + 0xaa, 0xa3, 0x36, + ]; + let s: [u8; 48] = [ + 0x66, 0xe4, 0x3d, 0xf2, 0x32, 0xf4, 0x70, 0x09, 0x57, 0x97, 0x67, 0x0f, 0x52, 0xcb, 0x5a, + 0xea, 0xd8, 0x65, 0x7c, 0x26, 0x41, 0xd1, 0x7a, 0x64, 0x75, 0x55, 0x7b, 0xbd, 0xe6, 0x48, + 0x8b, 0xd4, 0xdf, 0xe7, 0x75, 0x4c, 0xe5, 0xfb, 0xe6, 0xbf, 0x06, 0xd8, 0x2f, 0xd6, 0x95, + 0x0c, 0x5e, 0x6b, + ]; + let der: [u8; 103] = [ + 0x30, 0x65, 0x02, 0x31, 0x00, 0xda, 0xe6, 0x75, 0x8d, 0xde, 0xf4, 0xca, 0x42, 0x50, 0x9e, + 0x3e, 0x99, 0x1b, 0x7b, 0x5d, 0xde, 0x45, 0x33, 0x51, 0x75, 0x7a, 0xda, 0x92, 0xab, 0xff, + 0x46, 0x9a, 0x00, 0x38, 0x12, 0x9a, 0x83, 0x07, 0xcb, 0x61, 0x2c, 0xd9, 0x0e, 0xa2, 0xbf, + 0xbe, 0x91, 0xf3, 0xa4, 0x01, 0xaa, 0xa3, 0x36, 0x02, 0x30, 0x66, 0xe4, 0x3d, 0xf2, 0x32, + 0xf4, 0x70, 0x09, 0x57, 0x97, 0x67, 0x0f, 0x52, 0xcb, 0x5a, 0xea, 0xd8, 0x65, 0x7c, 0x26, + 0x41, 0xd1, 0x7a, 0x64, 0x75, 0x55, 0x7b, 0xbd, 0xe6, 0x48, 0x8b, 0xd4, 0xdf, 0xe7, 0x75, + 0x4c, 0xe5, 0xfb, 0xe6, 0xbf, 0x06, 0xd8, 0x2f, 0xd6, 0x95, 0x0c, 0x5e, 0x6b, + ]; + assert_eq!(max_len(48), 104); + round_trip(&r, &s, &der); +} + +#[test] +fn known_answer_p521_width_needs_long_form_sequence_length() { + let r: [u8; 66] = [ + 0x98, 0x75, 0x61, 0x66, 0xf7, 0x56, 0x00, 0x55, 0x83, 0x14, 0x3a, 0x81, 0xc4, 0x2d, 0x75, + 0x24, 0x06, 0x41, 0x2c, 0xc4, 0x85, 0x64, 0x39, 0xec, 0xe1, 0x3d, 0x35, 0xc9, 0x75, 0x84, + 0xbf, 0x8e, 0x55, 0x30, 0x80, 0xa1, 0x80, 0x16, 0xf1, 0x1c, 0xfe, 0x73, 0xec, 0x3e, 0x55, + 0x64, 0xf6, 0x7d, 0xed, 0x41, 0xee, 0xce, 0x2b, 0xeb, 0xa2, 0xdc, 0x71, 0xf5, 0x50, 0x78, + 0x1d, 0x99, 0x57, 0x0d, 0x1d, 0x85, + ]; + let s: [u8; 66] = [ + 0xd6, 0xcb, 0x5a, 0xf0, 0xb8, 0x1b, 0x5c, 0xef, 0x92, 0x40, 0xdb, 0xd9, 0xce, 0xaf, 0x3e, + 0xd9, 0x86, 0x43, 0x0d, 0x12, 0xce, 0x9a, 0xda, 0x86, 0x61, 0xf4, 0xf7, 0x7b, 0x45, 0x69, + 0xc7, 0x4c, 0x2c, 0xd5, 0x20, 0xad, 0xb4, 0x6b, 0xec, 0x91, 0x60, 0x03, 0x8e, 0x33, 0x9b, + 0x16, 0x71, 0x00, 0xe2, 0xa0, 0xf8, 0xce, 0x2d, 0x13, 0xd1, 0xd0, 0x79, 0x07, 0x2f, 0xbf, + 0x3d, 0x2a, 0xb7, 0x27, 0x26, 0x18, + ]; + let der: [u8; 141] = [ + 0x30, 0x81, 0x8a, 0x02, 0x43, 0x00, 0x98, 0x75, 0x61, 0x66, 0xf7, 0x56, 0x00, 0x55, 0x83, + 0x14, 0x3a, 0x81, 0xc4, 0x2d, 0x75, 0x24, 0x06, 0x41, 0x2c, 0xc4, 0x85, 0x64, 0x39, 0xec, + 0xe1, 0x3d, 0x35, 0xc9, 0x75, 0x84, 0xbf, 0x8e, 0x55, 0x30, 0x80, 0xa1, 0x80, 0x16, 0xf1, + 0x1c, 0xfe, 0x73, 0xec, 0x3e, 0x55, 0x64, 0xf6, 0x7d, 0xed, 0x41, 0xee, 0xce, 0x2b, 0xeb, + 0xa2, 0xdc, 0x71, 0xf5, 0x50, 0x78, 0x1d, 0x99, 0x57, 0x0d, 0x1d, 0x85, 0x02, 0x43, 0x00, + 0xd6, 0xcb, 0x5a, 0xf0, 0xb8, 0x1b, 0x5c, 0xef, 0x92, 0x40, 0xdb, 0xd9, 0xce, 0xaf, 0x3e, + 0xd9, 0x86, 0x43, 0x0d, 0x12, 0xce, 0x9a, 0xda, 0x86, 0x61, 0xf4, 0xf7, 0x7b, 0x45, 0x69, + 0xc7, 0x4c, 0x2c, 0xd5, 0x20, 0xad, 0xb4, 0x6b, 0xec, 0x91, 0x60, 0x03, 0x8e, 0x33, 0x9b, + 0x16, 0x71, 0x00, 0xe2, 0xa0, 0xf8, 0xce, 0x2d, 0x13, 0xd1, 0xd0, 0x79, 0x07, 0x2f, 0xbf, + 0x3d, 0x2a, 0xb7, 0x27, 0x26, 0x18, + ]; + assert_eq!(max_len(66), 141); + round_trip(&r, &s, &der); +} + +#[test] +fn known_answer_leading_zero_bytes_are_stripped() { + let r: [u8; 32] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0xdb, 0xf6, 0xe5, 0x8b, + 0xd9, 0x78, 0x0f, 0xc3, 0x9f, 0x02, 0x17, 0x24, 0xaa, 0x45, 0x1e, 0x83, 0xdf, 0x39, 0x7f, + 0xb5, 0xfd, + ]; + let s: [u8; 32] = [ + 0x19, 0x72, 0x06, 0x95, 0x3e, 0x09, 0x64, 0xbe, 0x7a, 0xe5, 0xe5, 0xc5, 0xb1, 0x87, 0x7d, + 0x19, 0xeb, 0xdb, 0xd8, 0x56, 0xbd, 0xee, 0x3f, 0xcf, 0xb7, 0x48, 0xa9, 0x47, 0x34, 0xfd, + 0x96, 0x5c, + ]; + let der: [u8; 60] = [ + 0x30, 0x3a, 0x02, 0x16, 0x05, 0xdb, 0xf6, 0xe5, 0x8b, 0xd9, 0x78, 0x0f, 0xc3, 0x9f, 0x02, + 0x17, 0x24, 0xaa, 0x45, 0x1e, 0x83, 0xdf, 0x39, 0x7f, 0xb5, 0xfd, 0x02, 0x20, 0x19, 0x72, + 0x06, 0x95, 0x3e, 0x09, 0x64, 0xbe, 0x7a, 0xe5, 0xe5, 0xc5, 0xb1, 0x87, 0x7d, 0x19, 0xeb, + 0xdb, 0xd8, 0x56, 0xbd, 0xee, 0x3f, 0xcf, 0xb7, 0x48, 0xa9, 0x47, 0x34, 0xfd, 0x96, 0x5c, + ]; + round_trip(&r, &s, &der); +} + +#[test] +fn known_answer_r_is_zero() { + let r = [0u8; 32]; + let s: [u8; 32] = [ + 0xad, 0x24, 0x8e, 0xb6, 0x65, 0xa3, 0x91, 0x5e, 0xd6, 0x06, 0x63, 0xdf, 0xd2, 0xf9, 0x87, + 0xad, 0x92, 0xc6, 0x34, 0xb1, 0x6f, 0x85, 0x5c, 0xe9, 0x19, 0xc4, 0xd6, 0x28, 0x7f, 0x33, + 0x88, 0xad, + ]; + let der: [u8; 40] = [ + 0x30, 0x26, 0x02, 0x01, 0x00, 0x02, 0x21, 0x00, 0xad, 0x24, 0x8e, 0xb6, 0x65, 0xa3, 0x91, + 0x5e, 0xd6, 0x06, 0x63, 0xdf, 0xd2, 0xf9, 0x87, 0xad, 0x92, 0xc6, 0x34, 0xb1, 0x6f, 0x85, + 0x5c, 0xe9, 0x19, 0xc4, 0xd6, 0x28, 0x7f, 0x33, 0x88, 0xad, + ]; + round_trip(&r, &s, &der); +} + +#[test] +fn max_len_boundary_at_the_short_to_long_form_transition() { + // content_len = 2*(n+3); n=60 -> 126 (< 0x80, short-form header), n=61 -> 128 (>= 0x80, + // long-form header). None of this crate's actual curve widths (32/48/66) land near this + // boundary, so it needs a dedicated, curve-independent check. + assert_eq!(max_len(60), 2 + 126); + assert_eq!(max_len(61), 3 + 128); +} + +#[test] +fn round_trip_at_the_sequence_content_length_128_boundary() { + // r, s both 61 bytes with the high bit set (forcing a pad byte each): every INTEGER TLV is + // 2 + 61 + 1 = 64 bytes, so the SEQUENCE content is exactly 128 bytes -- the smallest width at + // which the SEQUENCE's own length must use the long form (0x81 0x80), per max_len's docs. + let r = [0xffu8; 61]; + let s = [0xffu8; 61]; + let mut out = vec![0u8; max_len(61)]; + let len = encode(&r, &s, &mut out); + assert_eq!(len, 131); + assert_eq!( + &out[..3], + &[0x30, 0x81, 0x80], + "SEQUENCE length must use the long form at exactly 128" + ); + + let mut r_out = vec![0u8; 61]; + let mut s_out = vec![0u8; 61]; + decode(&out[..len], &mut r_out, &mut s_out) + .expect("must accept a minimal long-form length of exactly 128"); + assert_eq!(r_out, r); + assert_eq!(s_out, s); +} + +#[test] +fn decode_rejects_indefinite_length_even_when_the_rest_looks_like_a_valid_128_byte_body() { + // Builds on the same 61-byte encoding as the test above: its SEQUENCE content is genuinely 128 + // valid bytes, so a decoder that mistreated the outer length octet 0x80 as "short form, value + // 128" (instead of the indefinite-length marker DER forbids) would find a declared length that + // matches the buffer and content that parses as two valid INTEGERs -- and wrongly accept it. + // This is what makes the test discriminate the bug from a coincidental reject, unlike a buffer + // where the trailing content doesn't line up. + let r = [0xffu8; 61]; + let s = [0xffu8; 61]; + let mut out = vec![0u8; max_len(61)]; + let len = encode(&r, &s, &mut out); + assert_eq!(&out[..3], &[0x30, 0x81, 0x80]); + + let mut malformed = vec![0x30, 0x80]; + malformed.extend_from_slice(&out[3..len]); + + let mut r_out = vec![0u8; 61]; + let mut s_out = vec![0u8; 61]; + assert!(decode(&malformed, &mut r_out, &mut s_out).is_none()); +} + +fn valid_der() -> ([u8; 32], [u8; 32], Vec) { + let r = [7u8; 32]; + let s = [9u8; 32]; + let mut out = vec![0u8; max_len(32)]; + let len = encode(&r, &s, &mut out); + out.truncate(len); + (r, s, out) +} + +#[test] +fn decode_rejects_trailing_bytes() { + let (_, _, mut der) = valid_der(); + der.push(0x00); + let mut r_out = [0u8; 32]; + let mut s_out = [0u8; 32]; + assert!(decode(&der, &mut r_out, &mut s_out).is_none(), "must reject a trailing extra byte"); +} + +#[test] +fn decode_rejects_truncated_input() { + let (_, _, der) = valid_der(); + let mut r_out = [0u8; 32]; + let mut s_out = [0u8; 32]; + for cut in 1..der.len() { + assert!( + decode(&der[..cut], &mut r_out, &mut s_out).is_none(), + "must reject truncation to {cut} bytes" + ); + } +} + +#[test] +fn decode_rejects_wrong_outer_tag() { + let (_, _, mut der) = valid_der(); + der[0] = 0x31; // SET instead of SEQUENCE + let mut r_out = [0u8; 32]; + let mut s_out = [0u8; 32]; + assert!(decode(&der, &mut r_out, &mut s_out).is_none()); +} + +#[test] +fn decode_rejects_wrong_integer_tag() { + let (_, _, mut der) = valid_der(); + der[2] = 0x03; // BIT STRING instead of INTEGER + let mut r_out = [0u8; 32]; + let mut s_out = [0u8; 32]; + assert!(decode(&der, &mut r_out, &mut s_out).is_none()); +} + +#[test] +fn decode_rejects_non_canonical_leading_zero_pad() { + // r's INTEGER content is `00 07`: its high bit is not set (0x07), so the 0x00 pad byte in + // front of it is redundant -- ITU-T X.690 §8.3.2 forbids exactly this. + let malformed: [u8; 9] = [0x30, 0x07, 0x02, 0x02, 0x00, 0x07, 0x02, 0x01, 0x09]; + let mut r_out = [0u8; 1]; + let mut s_out = [0u8; 1]; + assert!(decode(&malformed, &mut r_out, &mut s_out).is_none()); +} + +#[test] +fn decode_rejects_negative_integer() { + // A lone 0x80 content byte has its high bit set with no 0x00 pad -- a negative two's-complement + // value, which r/s can never legitimately be. + let malformed: [u8; 7] = [0x30, 0x05, 0x02, 0x01, 0x80, 0x02, 0x00]; + let mut r_out = [0u8; 1]; + let mut s_out = [0u8; 1]; + // second INTEGER's content_len 0 also invalid (empty content), but the negative check on the + // first INTEGER must reject before that's even reached. + assert!(decode(&malformed, &mut r_out, &mut s_out).is_none()); +} + +#[test] +fn decode_rejects_indefinite_length() { + // 0x80 as the outer SEQUENCE's length octet is BER's indefinite-length form (X.690 §8.1.3.6), + // not a definite length; DER (§10.1) requires the definite form. + let der: [u8; 9] = [0x30, 0x80, 0x02, 0x01, 0x07, 0x02, 0x01, 0x09, 0x00]; + let mut r_out = [0u8; 1]; + let mut s_out = [0u8; 1]; + assert!(decode(&der, &mut r_out, &mut s_out).is_none()); +} + +#[test] +fn decode_rejects_non_minimal_long_form_length() { + // SEQUENCE content is 6 bytes (fits in the short form), but this encodes its length with the + // long form anyway (0x81 0x06) -- ITU-T X.690 §10.1 requires the minimum number of octets. + let malformed: [u8; 10] = [0x30, 0x81, 0x06, 0x02, 0x01, 0x07, 0x02, 0x01, 0x09, 0x00]; + let mut r_out = [0u8; 1]; + let mut s_out = [0u8; 1]; + assert!(decode(&malformed, &mut r_out, &mut s_out).is_none()); +} + +#[test] +fn decode_rejects_integer_wider_than_output() { + let (_, _, der) = valid_der(); + let mut r_out = [0u8; 16]; // r is actually 32 bytes wide + let mut s_out = [0u8; 16]; + assert!(decode(&der, &mut r_out, &mut s_out).is_none()); +} + +#[test] +fn decode_rejects_mismatched_output_widths() { + let (_, _, der) = valid_der(); + let mut r_out = [0u8; 32]; + let mut s_out = [0u8; 16]; + assert!(decode(&der, &mut r_out, &mut s_out).is_none()); +} + +/// xorshift64* PRNG, fixed seed: same rationale as the `ec` crate's field-arithmetic property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + + fn next_bytes(&mut self, n: usize) -> Vec { + let mut out = Vec::with_capacity(n); + while out.len() < n { + out.extend_from_slice(&self.next_u64().to_le_bytes()); + } + out.truncate(n); + out + } +} + +#[test] +fn round_trips_over_many_pseudorandom_widths_and_values() { + let mut rng = Xorshift64(0xD1CE5EED_D1CE5EEDu64); + for &n in &[32usize, 48, 66] { + for _ in 0..200 { + let r = rng.next_bytes(n); + let s = rng.next_bytes(n); + let mut out = vec![0u8; max_len(n)]; + let len = encode(&r, &s, &mut out); + assert!(len <= max_len(n)); + + let mut r_out = vec![0u8; n]; + let mut s_out = vec![0u8; n]; + decode(&out[..len], &mut r_out, &mut s_out).expect("round trip must decode"); + assert_eq!(r_out, r); + assert_eq!(s_out, s); + + // Any single flipped bit in a well-formed encoding must not silently decode to a + // *different* valid-looking (r, s) pair with a truncated read -- either it's rejected, + // or (rarely, e.g. a bit flip inside an unused zero-padding position that DER forbids + // anyway) it still equals the original; check it's never a mismatched success. + for bit_pos in [0usize, len.saturating_sub(1)] { + let mut flipped = out[..len].to_vec(); + flipped[bit_pos] ^= 0xFF; + let mut fr = vec![0u8; n]; + let mut fs = vec![0u8; n]; + if decode(&flipped, &mut fr, &mut fs).is_some() { + assert!( + fr != r || fs != s, + "corrupting byte {bit_pos} must not be silently ok" + ); + } + } + } + } +} + +#[test] +fn decode_rejects_empty_integer_content() { + // X.690 §8.3.1: an INTEGER's content is one or more octets, so `02 00` is malformed on its own + // (unlike `decode_rejects_negative_integer`, where the empty second INTEGER sits behind a + // negative first one that is rejected before it is ever reached). The second INTEGER is + // well-formed, so the empty first one is the only thing that can be rejecting this. + let malformed: [u8; 7] = [0x30, 0x05, 0x02, 0x00, 0x02, 0x01, 0x09]; + let mut r_out = [0u8; 1]; + let mut s_out = [0u8; 1]; + assert!(decode(&malformed, &mut r_out, &mut s_out).is_none()); +} + +#[test] +fn decode_rejects_sequence_content_not_exactly_filled_by_the_two_integers() { + // The SEQUENCE's declared length matches the buffer (so this is neither truncation nor + // trailing bytes after the SEQUENCE), and both INTEGERs are well-formed, but two more bytes + // sit inside the SEQUENCE after them: `Ecdsa-Sig-Value` has exactly two components. + let malformed: [u8; 10] = [0x30, 0x08, 0x02, 0x01, 0x07, 0x02, 0x01, 0x09, 0x05, 0x00]; + let mut r_out = [0u8; 1]; + let mut s_out = [0u8; 1]; + assert!(decode(&malformed, &mut r_out, &mut s_out).is_none()); +} diff --git a/crypto/ecdsa/tests/ecdsa_bp256r1_tests.rs b/crypto/ecdsa/tests/ecdsa_bp256r1_tests.rs new file mode 100644 index 000000000..55c1d6f5a --- /dev/null +++ b/crypto/ecdsa/tests/ecdsa_bp256r1_tests.rs @@ -0,0 +1,345 @@ +//! Behavioural tests for [`ECDSABp256r1`], mirroring `ecdsa_p256_tests.rs`. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_core_test_framework::signature::{ + TestFrameworkSignature, TestFrameworkSignatureKeys, +}; +use bouncycastle_ecdsa::ecdsa_bp256r1::ECDSABp256r1; +use bouncycastle_ecdsa::keys_bp256r1::{ECDSABp256r1PrivateKey, ECDSABp256r1PublicKey, keygen}; +use bouncycastle_ecdsa::keys_common::DerivePublicKey; +use bouncycastle_rng::DefaultRNG; + +#[test] +fn derive_pk_matches_keygen() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_eq!(sk.derive_pk(), pk); +} + +/// [`ECDSABp256r1`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. +#[test] +fn conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework.test_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDSABp256r1`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework.test_deterministic_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let deterministic = sk.sign_deterministic(msg, None).unwrap(); + pk.verify(msg, None, &random).unwrap(); + pk.verify(msg, None, &deterministic).unwrap(); +} + +#[test] +fn key_boundary_conditions() { + TestFrameworkSignatureKeys::new() + .test_keys::(|| { + keygen(&mut DefaultRNG::default()) + }); +} + +#[test] +fn sign_rng_produces_distinct_valid_signatures() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"randomised ECDSA signing"; + + let sig1 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let sig2 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); + + pk.verify(msg, None, &sig1).unwrap(); + pk.verify(msg, None, &sig2).unwrap(); +} + +#[test] +fn sign_rng_signature_rejected_under_wrong_key() { + let (_, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let (other_pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + match other_pk.verify(msg, None, &sig) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("expected SignatureVerificationFailed, got {other:?}"), + } +} + +#[test] +fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing"; + + let raw = sk.sign_deterministic(msg, None).unwrap(); + let (der, der_len) = sk.sign_der_deterministic(msg, None).unwrap(); + + let mut r_out = [0u8; 32]; + let mut s_out = [0u8; 32]; + bouncycastle_ecdsa::der::decode(&der[..der_len], &mut r_out, &mut s_out) + .expect("sign_der's output must be a well-formed DER SEQUENCE { r, s }"); + assert_eq!(r_out, raw[..32], "DER-decoded r must match sign's raw r"); + assert_eq!(s_out, raw[32..], "DER-decoded s must match sign's raw s"); + + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + + let mut tampered = der; + tampered[der_len - 1] ^= 0xFF; + assert!( + pk.verify_der(msg, None, &tampered[..der_len]).is_err(), + "a corrupted DER signature must not verify" + ); +} + +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = sk.sign_der(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); +} + +#[test] +fn public_key_uncompressed_and_compressed_round_trip() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let uncompressed = pk.encode(); + assert_eq!(uncompressed[0], 0x04); + assert_eq!(ECDSABp256r1PublicKey::from_bytes(&uncompressed).unwrap(), pk); + + let compressed_tag = if uncompressed[64] & 1 == 0 { 0x02 } else { 0x03 }; + let mut compressed = [0u8; 33]; + compressed[0] = compressed_tag; + compressed[1..].copy_from_slice(&uncompressed[1..33]); + assert_eq!(ECDSABp256r1PublicKey::from_bytes(&compressed).unwrap(), pk); +} + +#[test] +fn private_key_encode_round_trips_exact_bytes() { + let mut bytes = [0u8; 32]; + bytes[31] = 0x2A; // d = 42, well within [1, n-1] + let sk = ECDSABp256r1PrivateKey::from_bytes(&bytes).unwrap(); + assert_eq!(sk.encode(), bytes); + + let mut out = [0xFFu8; 32]; + let written = sk.encode_out(&mut out); + assert_eq!(written, 32); + assert_eq!(out, bytes); +} + +#[test] +fn public_key_encode_out_matches_encode() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let encoded = pk.encode(); + + let mut out = [0xFFu8; 65]; + let written = pk.encode_out(&mut out); + assert_eq!(written, 65); + assert_eq!(out, encoded); +} + +#[test] +fn public_key_display_differs_for_different_keys() { + let (pk1, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let (pk2, _) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_ne!(format!("{pk1}"), format!("{pk2}"), "Display should reflect the actual key bytes"); +} + +#[test] +fn private_key_rejects_zero() { + match ECDSABp256r1PrivateKey::from_bytes(&[0u8; 32]) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("expected DecodingError, got {other:?}"), + } +} + +#[test] +fn keygen_is_deterministic_given_a_deterministic_rng() { + // A fixed-output RNG (all-0x42 bytes) must yield the same key pair both times: pins that + // `keygen` has no hidden extra randomness source of its own. + struct FixedRng; + impl RNG for FixedRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0x42424242) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0x42u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0x42); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::_256bit + } + } + + let (pk1, sk1) = bouncycastle_ecdsa::keys_bp256r1::keygen(&mut FixedRng).unwrap(); + let (pk2, sk2) = bouncycastle_ecdsa::keys_bp256r1::keygen(&mut FixedRng).unwrap(); + assert_eq!(sk1, sk2); + assert_eq!(pk1, pk2); +} + +/// FIPS 186-5 Appendix A.2.1 step 3: an RNG weaker than the 128-bit security strength +/// brainpoolP256r1's order calls for (SP 800-57 Part 1 Rev. 5, Table 2) is refused rather than silently +/// used. +#[test] +fn keygen_rejects_an_rng_below_the_required_strength() { + struct WeakRng; + impl RNG for WeakRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::None + } + } + + assert!(matches!( + bouncycastle_ecdsa::keys_bp256r1::keygen(&mut WeakRng), + Err(SignatureError::RNGError( + bouncycastle_core::errors::RNGError::SecurityStrengthInsufficientForAlgorithm + )) + )); +} + +/// FIPS 186-5 §6.2 puts `d` in `[1, n-1]`, so an encoding of `n` or anything above it is not a +/// key. Reducing such an encoding mod `n` instead of rejecting it would give a single key two (in +/// fact, unboundedly many) valid-looking encodings: `d = n + 1` would load as `d = 1`. +#[test] +fn private_key_rejects_values_at_or_above_the_group_order() { + let n = bouncycastle_ec::bp256r1_sec1::be_bytes_from_limbs( + &bouncycastle_ec::bp256r1_scalar::N_LIMBS, + ); + let mut n_plus_1 = n; + n_plus_1[31] += 1; // n's last byte is well below 0xff, so this never carries + + for (name, bytes) in [("n", n), ("n + 1", n_plus_1), ("2^256 - 1", [0xffu8; 32])] { + match ECDSABp256r1PrivateKey::from_bytes(&bytes) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("d = {name} should have been rejected, got {other:?}"), + } + } + + // n - 1 is the largest valid d and must still load, and must not collide with any of the above. + let mut n_minus_1 = n; + n_minus_1[31] -= 1; // n is odd, so this never borrows + let sk = ECDSABp256r1PrivateKey::from_bytes(&n_minus_1).expect("d = n - 1 is in range"); + assert_eq!(sk.encode(), n_minus_1, "d = n - 1 must round-trip unchanged"); +} + +/// The raw encoding is exactly `r || s`; anything longer or shorter is a different, malformed +/// encoding, not a signature in a roomy buffer. Accepting trailing bytes would turn one valid +/// signature into unlimited distinct byte strings that all verify. +#[test] +fn verify_rejects_signature_of_the_wrong_length() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"length-checked signature"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + for extra in 1..=3 { + let mut too_long = sig.to_vec(); + too_long.extend(core::iter::repeat_n(0xAAu8, extra)); + match pk.verify(msg, None, &too_long) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{extra} trailing byte(s) should have failed, got {other:?}"), + } + } + + for short in 1..=3 { + match pk.verify(msg, None, &sig[..64 - short]) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{short} byte(s) short should have failed, got {other:?}"), + } + } +} + +/// FIPS 186-5 §6.4.2 step 1: `r` and `s` must each be in `[1, n-1]`, and an out-of-range value is +/// rejected rather than reduced. Each half of a valid signature is replaced in turn by `0`, by +/// `n`, and by the all-ones pattern; the untouched half stays valid, so only the range check can +/// be what rejects the result. The wycheproof suites cover this too, but only for the curves +/// and encodings they happen to include, and not by name. +#[test] +fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"range-checked r and s"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + let n = bouncycastle_ec::bp256r1_sec1::be_bytes_from_limbs( + &bouncycastle_ec::bp256r1_scalar::N_LIMBS, + ); + for (name, bad) in [("0", [0u8; 32]), ("n", n), ("2^256 - 1", [0xffu8; 32])] { + for (half, offset) in [("r", 0usize), ("s", 32usize)] { + let mut tampered = sig; + tampered[offset..offset + 32].copy_from_slice(&bad); + match pk.verify(msg, None, &tampered) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("{half} = {name} should have been rejected, got {other:?}"), + } + } + } +} diff --git a/crypto/ecdsa/tests/ecdsa_bp384r1_tests.rs b/crypto/ecdsa/tests/ecdsa_bp384r1_tests.rs new file mode 100644 index 000000000..1b35132d3 --- /dev/null +++ b/crypto/ecdsa/tests/ecdsa_bp384r1_tests.rs @@ -0,0 +1,345 @@ +//! Behavioural tests for [`ECDSABp384r1`], mirroring `ecdsa_p256_tests.rs`. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_core_test_framework::signature::{ + TestFrameworkSignature, TestFrameworkSignatureKeys, +}; +use bouncycastle_ecdsa::ecdsa_bp384r1::ECDSABp384r1; +use bouncycastle_ecdsa::keys_bp384r1::{ECDSABp384r1PrivateKey, ECDSABp384r1PublicKey, keygen}; +use bouncycastle_ecdsa::keys_common::DerivePublicKey; +use bouncycastle_rng::DefaultRNG; + +#[test] +fn derive_pk_matches_keygen() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_eq!(sk.derive_pk(), pk); +} + +/// [`ECDSABp384r1`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. +#[test] +fn conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework.test_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDSABp384r1`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework.test_deterministic_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let deterministic = sk.sign_deterministic(msg, None).unwrap(); + pk.verify(msg, None, &random).unwrap(); + pk.verify(msg, None, &deterministic).unwrap(); +} + +#[test] +fn key_boundary_conditions() { + TestFrameworkSignatureKeys::new() + .test_keys::(|| { + keygen(&mut DefaultRNG::default()) + }); +} + +#[test] +fn sign_rng_produces_distinct_valid_signatures() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"randomised ECDSA signing"; + + let sig1 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let sig2 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); + + pk.verify(msg, None, &sig1).unwrap(); + pk.verify(msg, None, &sig2).unwrap(); +} + +#[test] +fn sign_rng_signature_rejected_under_wrong_key() { + let (_, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let (other_pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + match other_pk.verify(msg, None, &sig) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("expected SignatureVerificationFailed, got {other:?}"), + } +} + +#[test] +fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing"; + + let raw = sk.sign_deterministic(msg, None).unwrap(); + let (der, der_len) = sk.sign_der_deterministic(msg, None).unwrap(); + + let mut r_out = [0u8; 48]; + let mut s_out = [0u8; 48]; + bouncycastle_ecdsa::der::decode(&der[..der_len], &mut r_out, &mut s_out) + .expect("sign_der's output must be a well-formed DER SEQUENCE { r, s }"); + assert_eq!(r_out, raw[..48], "DER-decoded r must match sign's raw r"); + assert_eq!(s_out, raw[48..], "DER-decoded s must match sign's raw s"); + + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + + let mut tampered = der; + tampered[der_len - 1] ^= 0xFF; + assert!( + pk.verify_der(msg, None, &tampered[..der_len]).is_err(), + "a corrupted DER signature must not verify" + ); +} + +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = sk.sign_der(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); +} + +#[test] +fn public_key_uncompressed_and_compressed_round_trip() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let uncompressed = pk.encode(); + assert_eq!(uncompressed[0], 0x04); + assert_eq!(ECDSABp384r1PublicKey::from_bytes(&uncompressed).unwrap(), pk); + + let compressed_tag = if uncompressed[96] & 1 == 0 { 0x02 } else { 0x03 }; + let mut compressed = [0u8; 49]; + compressed[0] = compressed_tag; + compressed[1..].copy_from_slice(&uncompressed[1..49]); + assert_eq!(ECDSABp384r1PublicKey::from_bytes(&compressed).unwrap(), pk); +} + +#[test] +fn private_key_encode_round_trips_exact_bytes() { + let mut bytes = [0u8; 48]; + bytes[47] = 0x2A; // d = 42, well within [1, n-1] + let sk = ECDSABp384r1PrivateKey::from_bytes(&bytes).unwrap(); + assert_eq!(sk.encode(), bytes); + + let mut out = [0xFFu8; 48]; + let written = sk.encode_out(&mut out); + assert_eq!(written, 48); + assert_eq!(out, bytes); +} + +#[test] +fn public_key_encode_out_matches_encode() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let encoded = pk.encode(); + + let mut out = [0xFFu8; 97]; + let written = pk.encode_out(&mut out); + assert_eq!(written, 97); + assert_eq!(out, encoded); +} + +#[test] +fn public_key_display_differs_for_different_keys() { + let (pk1, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let (pk2, _) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_ne!(format!("{pk1}"), format!("{pk2}"), "Display should reflect the actual key bytes"); +} + +#[test] +fn private_key_rejects_zero() { + match ECDSABp384r1PrivateKey::from_bytes(&[0u8; 48]) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("expected DecodingError, got {other:?}"), + } +} + +#[test] +fn keygen_is_deterministic_given_a_deterministic_rng() { + // A fixed-output RNG (all-0x42 bytes) must yield the same key pair both times: pins that + // `keygen` has no hidden extra randomness source of its own. + struct FixedRng; + impl RNG for FixedRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0x42424242) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0x42u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0x42); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::_256bit + } + } + + let (pk1, sk1) = bouncycastle_ecdsa::keys_bp384r1::keygen(&mut FixedRng).unwrap(); + let (pk2, sk2) = bouncycastle_ecdsa::keys_bp384r1::keygen(&mut FixedRng).unwrap(); + assert_eq!(sk1, sk2); + assert_eq!(pk1, pk2); +} + +/// FIPS 186-5 Appendix A.2.1 step 3: an RNG weaker than the 192-bit security strength +/// brainpoolP384r1's order calls for (SP 800-57 Part 1 Rev. 5, Table 2) is refused rather than silently +/// used. +#[test] +fn keygen_rejects_an_rng_below_the_required_strength() { + struct WeakRng; + impl RNG for WeakRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::None + } + } + + assert!(matches!( + bouncycastle_ecdsa::keys_bp384r1::keygen(&mut WeakRng), + Err(SignatureError::RNGError( + bouncycastle_core::errors::RNGError::SecurityStrengthInsufficientForAlgorithm + )) + )); +} + +/// FIPS 186-5 §6.2 puts `d` in `[1, n-1]`, so an encoding of `n` or anything above it is not a +/// key. Reducing such an encoding mod `n` instead of rejecting it would give a single key two (in +/// fact, unboundedly many) valid-looking encodings: `d = n + 1` would load as `d = 1`. +#[test] +fn private_key_rejects_values_at_or_above_the_group_order() { + let n = bouncycastle_ec::bp384r1_sec1::be_bytes_from_limbs( + &bouncycastle_ec::bp384r1_scalar::N_LIMBS, + ); + let mut n_plus_1 = n; + n_plus_1[47] += 1; // n's last byte is well below 0xff, so this never carries + + for (name, bytes) in [("n", n), ("n + 1", n_plus_1), ("2^384 - 1", [0xffu8; 48])] { + match ECDSABp384r1PrivateKey::from_bytes(&bytes) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("d = {name} should have been rejected, got {other:?}"), + } + } + + // n - 1 is the largest valid d and must still load, and must not collide with any of the above. + let mut n_minus_1 = n; + n_minus_1[47] -= 1; // n is odd, so this never borrows + let sk = ECDSABp384r1PrivateKey::from_bytes(&n_minus_1).expect("d = n - 1 is in range"); + assert_eq!(sk.encode(), n_minus_1, "d = n - 1 must round-trip unchanged"); +} + +/// The raw encoding is exactly `r || s`; anything longer or shorter is a different, malformed +/// encoding, not a signature in a roomy buffer. Accepting trailing bytes would turn one valid +/// signature into unlimited distinct byte strings that all verify. +#[test] +fn verify_rejects_signature_of_the_wrong_length() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"length-checked signature"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + for extra in 1..=3 { + let mut too_long = sig.to_vec(); + too_long.extend(core::iter::repeat_n(0xAAu8, extra)); + match pk.verify(msg, None, &too_long) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{extra} trailing byte(s) should have failed, got {other:?}"), + } + } + + for short in 1..=3 { + match pk.verify(msg, None, &sig[..96 - short]) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{short} byte(s) short should have failed, got {other:?}"), + } + } +} + +/// FIPS 186-5 §6.4.2 step 1: `r` and `s` must each be in `[1, n-1]`, and an out-of-range value is +/// rejected rather than reduced. Each half of a valid signature is replaced in turn by `0`, by +/// `n`, and by the all-ones pattern; the untouched half stays valid, so only the range check can +/// be what rejects the result. The wycheproof suites cover this too, but only for the curves +/// and encodings they happen to include, and not by name. +#[test] +fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"range-checked r and s"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + let n = bouncycastle_ec::bp384r1_sec1::be_bytes_from_limbs( + &bouncycastle_ec::bp384r1_scalar::N_LIMBS, + ); + for (name, bad) in [("0", [0u8; 48]), ("n", n), ("2^384 - 1", [0xffu8; 48])] { + for (half, offset) in [("r", 0usize), ("s", 48usize)] { + let mut tampered = sig; + tampered[offset..offset + 48].copy_from_slice(&bad); + match pk.verify(msg, None, &tampered) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("{half} = {name} should have been rejected, got {other:?}"), + } + } + } +} diff --git a/crypto/ecdsa/tests/ecdsa_bp512r1_tests.rs b/crypto/ecdsa/tests/ecdsa_bp512r1_tests.rs new file mode 100644 index 000000000..0833d3692 --- /dev/null +++ b/crypto/ecdsa/tests/ecdsa_bp512r1_tests.rs @@ -0,0 +1,345 @@ +//! Behavioural tests for [`ECDSABp512r1`], mirroring `ecdsa_p256_tests.rs`. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_core_test_framework::signature::{ + TestFrameworkSignature, TestFrameworkSignatureKeys, +}; +use bouncycastle_ecdsa::ecdsa_bp512r1::ECDSABp512r1; +use bouncycastle_ecdsa::keys_bp512r1::{ECDSABp512r1PrivateKey, ECDSABp512r1PublicKey, keygen}; +use bouncycastle_ecdsa::keys_common::DerivePublicKey; +use bouncycastle_rng::DefaultRNG; + +#[test] +fn derive_pk_matches_keygen() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_eq!(sk.derive_pk(), pk); +} + +/// [`ECDSABp512r1`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. +#[test] +fn conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework.test_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDSABp512r1`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework.test_deterministic_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let deterministic = sk.sign_deterministic(msg, None).unwrap(); + pk.verify(msg, None, &random).unwrap(); + pk.verify(msg, None, &deterministic).unwrap(); +} + +#[test] +fn key_boundary_conditions() { + TestFrameworkSignatureKeys::new() + .test_keys::(|| { + keygen(&mut DefaultRNG::default()) + }); +} + +#[test] +fn sign_rng_produces_distinct_valid_signatures() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"randomised ECDSA signing"; + + let sig1 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let sig2 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); + + pk.verify(msg, None, &sig1).unwrap(); + pk.verify(msg, None, &sig2).unwrap(); +} + +#[test] +fn sign_rng_signature_rejected_under_wrong_key() { + let (_, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let (other_pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + match other_pk.verify(msg, None, &sig) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("expected SignatureVerificationFailed, got {other:?}"), + } +} + +#[test] +fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing"; + + let raw = sk.sign_deterministic(msg, None).unwrap(); + let (der, der_len) = sk.sign_der_deterministic(msg, None).unwrap(); + + let mut r_out = [0u8; 64]; + let mut s_out = [0u8; 64]; + bouncycastle_ecdsa::der::decode(&der[..der_len], &mut r_out, &mut s_out) + .expect("sign_der's output must be a well-formed DER SEQUENCE { r, s }"); + assert_eq!(r_out, raw[..64], "DER-decoded r must match sign's raw r"); + assert_eq!(s_out, raw[64..], "DER-decoded s must match sign's raw s"); + + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + + let mut tampered = der; + tampered[der_len - 1] ^= 0xFF; + assert!( + pk.verify_der(msg, None, &tampered[..der_len]).is_err(), + "a corrupted DER signature must not verify" + ); +} + +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = sk.sign_der(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); +} + +#[test] +fn public_key_uncompressed_and_compressed_round_trip() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let uncompressed = pk.encode(); + assert_eq!(uncompressed[0], 0x04); + assert_eq!(ECDSABp512r1PublicKey::from_bytes(&uncompressed).unwrap(), pk); + + let compressed_tag = if uncompressed[128] & 1 == 0 { 0x02 } else { 0x03 }; + let mut compressed = [0u8; 65]; + compressed[0] = compressed_tag; + compressed[1..].copy_from_slice(&uncompressed[1..65]); + assert_eq!(ECDSABp512r1PublicKey::from_bytes(&compressed).unwrap(), pk); +} + +#[test] +fn private_key_encode_round_trips_exact_bytes() { + let mut bytes = [0u8; 64]; + bytes[63] = 0x2A; // d = 42, well within [1, n-1] + let sk = ECDSABp512r1PrivateKey::from_bytes(&bytes).unwrap(); + assert_eq!(sk.encode(), bytes); + + let mut out = [0xFFu8; 64]; + let written = sk.encode_out(&mut out); + assert_eq!(written, 64); + assert_eq!(out, bytes); +} + +#[test] +fn public_key_encode_out_matches_encode() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let encoded = pk.encode(); + + let mut out = [0xFFu8; 129]; + let written = pk.encode_out(&mut out); + assert_eq!(written, 129); + assert_eq!(out, encoded); +} + +#[test] +fn public_key_display_differs_for_different_keys() { + let (pk1, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let (pk2, _) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_ne!(format!("{pk1}"), format!("{pk2}"), "Display should reflect the actual key bytes"); +} + +#[test] +fn private_key_rejects_zero() { + match ECDSABp512r1PrivateKey::from_bytes(&[0u8; 64]) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("expected DecodingError, got {other:?}"), + } +} + +#[test] +fn keygen_is_deterministic_given_a_deterministic_rng() { + // A fixed-output RNG (all-0x42 bytes) must yield the same key pair both times: pins that + // `keygen` has no hidden extra randomness source of its own. + struct FixedRng; + impl RNG for FixedRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0x42424242) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0x42u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0x42); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::_256bit + } + } + + let (pk1, sk1) = bouncycastle_ecdsa::keys_bp512r1::keygen(&mut FixedRng).unwrap(); + let (pk2, sk2) = bouncycastle_ecdsa::keys_bp512r1::keygen(&mut FixedRng).unwrap(); + assert_eq!(sk1, sk2); + assert_eq!(pk1, pk2); +} + +/// FIPS 186-5 Appendix A.2.1 step 3: an RNG weaker than the 256-bit security strength +/// brainpoolP512r1's order calls for (SP 800-57 Part 1 Rev. 5, Table 2) is refused rather than silently +/// used. +#[test] +fn keygen_rejects_an_rng_below_the_required_strength() { + struct WeakRng; + impl RNG for WeakRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::None + } + } + + assert!(matches!( + bouncycastle_ecdsa::keys_bp512r1::keygen(&mut WeakRng), + Err(SignatureError::RNGError( + bouncycastle_core::errors::RNGError::SecurityStrengthInsufficientForAlgorithm + )) + )); +} + +/// FIPS 186-5 §6.2 puts `d` in `[1, n-1]`, so an encoding of `n` or anything above it is not a +/// key. Reducing such an encoding mod `n` instead of rejecting it would give a single key two (in +/// fact, unboundedly many) valid-looking encodings: `d = n + 1` would load as `d = 1`. +#[test] +fn private_key_rejects_values_at_or_above_the_group_order() { + let n = bouncycastle_ec::bp512r1_sec1::be_bytes_from_limbs( + &bouncycastle_ec::bp512r1_scalar::N_LIMBS, + ); + let mut n_plus_1 = n; + n_plus_1[63] += 1; // n's last byte is well below 0xff, so this never carries + + for (name, bytes) in [("n", n), ("n + 1", n_plus_1), ("2^512 - 1", [0xffu8; 64])] { + match ECDSABp512r1PrivateKey::from_bytes(&bytes) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("d = {name} should have been rejected, got {other:?}"), + } + } + + // n - 1 is the largest valid d and must still load, and must not collide with any of the above. + let mut n_minus_1 = n; + n_minus_1[63] -= 1; // n is odd, so this never borrows + let sk = ECDSABp512r1PrivateKey::from_bytes(&n_minus_1).expect("d = n - 1 is in range"); + assert_eq!(sk.encode(), n_minus_1, "d = n - 1 must round-trip unchanged"); +} + +/// The raw encoding is exactly `r || s`; anything longer or shorter is a different, malformed +/// encoding, not a signature in a roomy buffer. Accepting trailing bytes would turn one valid +/// signature into unlimited distinct byte strings that all verify. +#[test] +fn verify_rejects_signature_of_the_wrong_length() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"length-checked signature"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + for extra in 1..=3 { + let mut too_long = sig.to_vec(); + too_long.extend(core::iter::repeat_n(0xAAu8, extra)); + match pk.verify(msg, None, &too_long) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{extra} trailing byte(s) should have failed, got {other:?}"), + } + } + + for short in 1..=3 { + match pk.verify(msg, None, &sig[..128 - short]) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{short} byte(s) short should have failed, got {other:?}"), + } + } +} + +/// FIPS 186-5 §6.4.2 step 1: `r` and `s` must each be in `[1, n-1]`, and an out-of-range value is +/// rejected rather than reduced. Each half of a valid signature is replaced in turn by `0`, by +/// `n`, and by the all-ones pattern; the untouched half stays valid, so only the range check can +/// be what rejects the result. The wycheproof suites cover this too, but only for the curves +/// and encodings they happen to include, and not by name. +#[test] +fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"range-checked r and s"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + let n = bouncycastle_ec::bp512r1_sec1::be_bytes_from_limbs( + &bouncycastle_ec::bp512r1_scalar::N_LIMBS, + ); + for (name, bad) in [("0", [0u8; 64]), ("n", n), ("2^512 - 1", [0xffu8; 64])] { + for (half, offset) in [("r", 0usize), ("s", 64usize)] { + let mut tampered = sig; + tampered[offset..offset + 64].copy_from_slice(&bad); + match pk.verify(msg, None, &tampered) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("{half} = {name} should have been rejected, got {other:?}"), + } + } + } +} diff --git a/crypto/ecdsa/tests/ecdsa_p256_tests.rs b/crypto/ecdsa/tests/ecdsa_p256_tests.rs new file mode 100644 index 000000000..fbcea7eab --- /dev/null +++ b/crypto/ecdsa/tests/ecdsa_p256_tests.rs @@ -0,0 +1,351 @@ +//! Behavioural tests for [`ECDSAP256`]: the shared `core-test-framework` conformance suite (per +//! QUALITY_AND_STYLE's rule that trait conformance lives there, not per-implementation), key +//! encode/decode boundary conditions, and this crate's own additions the generic suite doesn't +//! cover -- the `sign_rng` path and public-key encoding round trips. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_core_test_framework::signature::{ + TestFrameworkSignature, TestFrameworkSignatureKeys, +}; +use bouncycastle_ec::p256_scalar::N_LIMBS; +use bouncycastle_ec::p256_sec1::be_bytes_from_limbs; +use bouncycastle_ecdsa::ecdsa_p256::ECDSAP256; +use bouncycastle_ecdsa::keys_common::DerivePublicKey; +use bouncycastle_ecdsa::keys_p256::{ECDSAP256PrivateKey, ECDSAP256PublicKey, keygen}; +use bouncycastle_rng::DefaultRNG; + +#[test] +fn derive_pk_matches_keygen() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_eq!(sk.derive_pk(), pk); +} + +/// [`ECDSAP256`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. +#[test] +fn conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework.test_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDSAP256`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework + .test_deterministic_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let deterministic = sk.sign_deterministic(msg, None).unwrap(); + pk.verify(msg, None, &random).unwrap(); + pk.verify(msg, None, &deterministic).unwrap(); +} + +#[test] +fn key_boundary_conditions() { + TestFrameworkSignatureKeys::new().test_keys::( + || keygen(&mut DefaultRNG::default()), + ); +} + +#[test] +fn sign_rng_produces_distinct_valid_signatures() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"randomised ECDSA signing"; + + let sig1 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let sig2 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); + + pk.verify(msg, None, &sig1).unwrap(); + pk.verify(msg, None, &sig2).unwrap(); +} + +#[test] +fn sign_rng_signature_rejected_under_wrong_key() { + let (_, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let (other_pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + match other_pk.verify(msg, None, &sig) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("expected SignatureVerificationFailed, got {other:?}"), + } +} + +#[test] +fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing"; + + let raw = sk.sign_deterministic(msg, None).unwrap(); + let (der, der_len) = sk.sign_der_deterministic(msg, None).unwrap(); + + let mut r_out = [0u8; 32]; + let mut s_out = [0u8; 32]; + bouncycastle_ecdsa::der::decode(&der[..der_len], &mut r_out, &mut s_out) + .expect("sign_der's output must be a well-formed DER SEQUENCE { r, s }"); + assert_eq!(r_out, raw[..32], "DER-decoded r must match sign's raw r"); + assert_eq!(s_out, raw[32..], "DER-decoded s must match sign's raw s"); + + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + + let mut tampered = der; + tampered[der_len - 1] ^= 0xFF; + assert!( + pk.verify_der(msg, None, &tampered[..der_len]).is_err(), + "a corrupted DER signature must not verify" + ); +} + +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = sk.sign_der(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); +} + +#[test] +fn public_key_uncompressed_and_compressed_round_trip() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let uncompressed = pk.encode(); + assert_eq!(uncompressed[0], 0x04); + assert_eq!(ECDSAP256PublicKey::from_bytes(&uncompressed).unwrap(), pk); + + let compressed_tag = if uncompressed[64] & 1 == 0 { 0x02 } else { 0x03 }; + let mut compressed = [0u8; 33]; + compressed[0] = compressed_tag; + compressed[1..].copy_from_slice(&uncompressed[1..33]); + assert_eq!(ECDSAP256PublicKey::from_bytes(&compressed).unwrap(), pk); +} + +#[test] +fn private_key_encode_round_trips_exact_bytes() { + let mut bytes = [0u8; 32]; + bytes[31] = 0x2A; // d = 42, well within [1, n-1] + let sk = ECDSAP256PrivateKey::from_bytes(&bytes).unwrap(); + assert_eq!(sk.encode(), bytes); + + let mut out = [0xFFu8; 32]; + let written = sk.encode_out(&mut out); + assert_eq!(written, 32); + assert_eq!(out, bytes); +} + +#[test] +fn public_key_encode_out_matches_encode() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let encoded = pk.encode(); + + let mut out = [0xFFu8; 65]; + let written = pk.encode_out(&mut out); + assert_eq!(written, 65); + assert_eq!(out, encoded); +} + +#[test] +fn public_key_display_differs_for_different_keys() { + let (pk1, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let (pk2, _) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_ne!(format!("{pk1}"), format!("{pk2}"), "Display should reflect the actual key bytes"); +} + +#[test] +fn private_key_rejects_zero() { + match ECDSAP256PrivateKey::from_bytes(&[0u8; 32]) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("expected DecodingError, got {other:?}"), + } +} + +/// FIPS 186-5 §6.2 puts `d` in `[1, n-1]`, so an encoding of `n` or anything above it is not a +/// key. Reducing such an encoding mod `n` instead of rejecting it would give a single key two (in +/// fact, unboundedly many) valid-looking encodings: `d = n + 1` would load as `d = 1`. +#[test] +fn private_key_rejects_values_at_or_above_the_group_order() { + let n = be_bytes_from_limbs(&N_LIMBS); + + for (name, bytes) in [("n", n), ("n + 1", add_one(&n)), ("2^256 - 1", [0xffu8; 32])] { + match ECDSAP256PrivateKey::from_bytes(&bytes) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("d = {name} should have been rejected, got {other:?}"), + } + } + + // n - 1 is the largest valid d and must still load, and must not collide with any of the above. + let mut n_minus_1 = n; + n_minus_1[31] -= 1; // N_LIMBS[0] is odd, so this never borrows + let sk = ECDSAP256PrivateKey::from_bytes(&n_minus_1).expect("d = n - 1 is in range"); + assert_eq!(sk.encode(), n_minus_1, "d = n - 1 must round-trip unchanged"); +} + +/// Big-endian `bytes + 1`; only ever called on a value whose last byte is not `0xff`. +fn add_one(bytes: &[u8; 32]) -> [u8; 32] { + let mut out = *bytes; + out[31] += 1; + out +} + +#[test] +fn keygen_is_deterministic_given_a_deterministic_rng() { + // A fixed-output RNG (all-0x42 bytes) must yield the same key pair both times: pins that + // `keygen` has no hidden extra randomness source of its own. + struct FixedRng; + impl RNG for FixedRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0x42424242) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0x42u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0x42); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::_256bit + } + } + + let (pk1, sk1) = bouncycastle_ecdsa::keys_p256::keygen(&mut FixedRng).unwrap(); + let (pk2, sk2) = bouncycastle_ecdsa::keys_p256::keygen(&mut FixedRng).unwrap(); + assert_eq!(sk1, sk2); + assert_eq!(pk1, pk2); +} + +/// FIPS 186-5 Appendix A.2.1 step 3: an RNG weaker than the 128-bit security strength +/// P-256's order calls for (SP 800-57 Part 1 Rev. 5, Table 2) is refused rather than silently +/// used. +#[test] +fn keygen_rejects_an_rng_below_the_required_strength() { + struct WeakRng; + impl RNG for WeakRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::None + } + } + + assert!(matches!( + bouncycastle_ecdsa::keys_p256::keygen(&mut WeakRng), + Err(SignatureError::RNGError( + bouncycastle_core::errors::RNGError::SecurityStrengthInsufficientForAlgorithm + )) + )); +} + +/// The raw encoding is exactly `r || s`; anything longer or shorter is a different, malformed +/// encoding, not a signature in a roomy buffer. Accepting trailing bytes would turn one valid +/// signature into unlimited distinct byte strings that all verify. +#[test] +fn verify_rejects_signature_of_the_wrong_length() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"length-checked signature"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + for extra in 1..=3 { + let mut too_long = sig.to_vec(); + too_long.extend(core::iter::repeat_n(0xAAu8, extra)); + match pk.verify(msg, None, &too_long) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{extra} trailing byte(s) should have failed, got {other:?}"), + } + } + + for short in 1..=3 { + match pk.verify(msg, None, &sig[..64 - short]) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{short} byte(s) short should have failed, got {other:?}"), + } + } +} + +/// FIPS 186-5 §6.4.2 step 1: `r` and `s` must each be in `[1, n-1]`, and an out-of-range value is +/// rejected rather than reduced. Each half of a valid signature is replaced in turn by `0`, by +/// `n`, and by the all-ones pattern; the untouched half stays valid, so only the range check can +/// be what rejects the result. The wycheproof suites cover this too, but only for the curves +/// and encodings they happen to include, and not by name. +#[test] +fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"range-checked r and s"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + let n = bouncycastle_ec::p256_sec1::be_bytes_from_limbs(&bouncycastle_ec::p256_scalar::N_LIMBS); + for (name, bad) in [("0", [0u8; 32]), ("n", n), ("2^256 - 1", [0xffu8; 32])] { + for (half, offset) in [("r", 0usize), ("s", 32usize)] { + let mut tampered = sig; + tampered[offset..offset + 32].copy_from_slice(&bad); + match pk.verify(msg, None, &tampered) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("{half} = {name} should have been rejected, got {other:?}"), + } + } + } +} diff --git a/crypto/ecdsa/tests/ecdsa_p256k1_tests.rs b/crypto/ecdsa/tests/ecdsa_p256k1_tests.rs new file mode 100644 index 000000000..63ddf271e --- /dev/null +++ b/crypto/ecdsa/tests/ecdsa_p256k1_tests.rs @@ -0,0 +1,344 @@ +//! Behavioural tests for [`ECDSASecp256K1`], mirroring `ecdsa_p256_tests.rs`. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_core_test_framework::signature::{ + TestFrameworkSignature, TestFrameworkSignatureKeys, +}; +use bouncycastle_ecdsa::ecdsa_p256k1::ECDSASecp256K1; +use bouncycastle_ecdsa::keys_common::DerivePublicKey; +use bouncycastle_ecdsa::keys_p256k1::{ECDSASecp256K1PrivateKey, ECDSASecp256K1PublicKey, keygen}; +use bouncycastle_rng::DefaultRNG; + +#[test] +fn derive_pk_matches_keygen() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_eq!(sk.derive_pk(), pk); +} + +/// [`ECDSASecp256K1`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. +#[test] +fn conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework + .test_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDSASecp256K1`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework.test_deterministic_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let deterministic = sk.sign_deterministic(msg, None).unwrap(); + pk.verify(msg, None, &random).unwrap(); + pk.verify(msg, None, &deterministic).unwrap(); +} + +#[test] +fn key_boundary_conditions() { + TestFrameworkSignatureKeys::new() + .test_keys::(|| { + keygen(&mut DefaultRNG::default()) + }); +} + +#[test] +fn sign_rng_produces_distinct_valid_signatures() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"randomised ECDSA signing"; + + let sig1 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let sig2 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); + + pk.verify(msg, None, &sig1).unwrap(); + pk.verify(msg, None, &sig2).unwrap(); +} + +#[test] +fn sign_rng_signature_rejected_under_wrong_key() { + let (_, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let (other_pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + match other_pk.verify(msg, None, &sig) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("expected SignatureVerificationFailed, got {other:?}"), + } +} + +#[test] +fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing"; + + let raw = sk.sign_deterministic(msg, None).unwrap(); + let (der, der_len) = sk.sign_der_deterministic(msg, None).unwrap(); + + let mut r_out = [0u8; 32]; + let mut s_out = [0u8; 32]; + bouncycastle_ecdsa::der::decode(&der[..der_len], &mut r_out, &mut s_out) + .expect("sign_der's output must be a well-formed DER SEQUENCE { r, s }"); + assert_eq!(r_out, raw[..32], "DER-decoded r must match sign's raw r"); + assert_eq!(s_out, raw[32..], "DER-decoded s must match sign's raw s"); + + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + + let mut tampered = der; + tampered[der_len - 1] ^= 0xFF; + assert!( + pk.verify_der(msg, None, &tampered[..der_len]).is_err(), + "a corrupted DER signature must not verify" + ); +} + +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = sk.sign_der(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); +} + +#[test] +fn public_key_uncompressed_and_compressed_round_trip() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let uncompressed = pk.encode(); + assert_eq!(uncompressed[0], 0x04); + assert_eq!(ECDSASecp256K1PublicKey::from_bytes(&uncompressed).unwrap(), pk); + + let compressed_tag = if uncompressed[64] & 1 == 0 { 0x02 } else { 0x03 }; + let mut compressed = [0u8; 33]; + compressed[0] = compressed_tag; + compressed[1..].copy_from_slice(&uncompressed[1..33]); + assert_eq!(ECDSASecp256K1PublicKey::from_bytes(&compressed).unwrap(), pk); +} + +#[test] +fn private_key_encode_round_trips_exact_bytes() { + let mut bytes = [0u8; 32]; + bytes[31] = 0x2A; // d = 42, well within [1, n-1] + let sk = ECDSASecp256K1PrivateKey::from_bytes(&bytes).unwrap(); + assert_eq!(sk.encode(), bytes); + + let mut out = [0xFFu8; 32]; + let written = sk.encode_out(&mut out); + assert_eq!(written, 32); + assert_eq!(out, bytes); +} + +#[test] +fn public_key_encode_out_matches_encode() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let encoded = pk.encode(); + + let mut out = [0xFFu8; 65]; + let written = pk.encode_out(&mut out); + assert_eq!(written, 65); + assert_eq!(out, encoded); +} + +#[test] +fn public_key_display_differs_for_different_keys() { + let (pk1, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let (pk2, _) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_ne!(format!("{pk1}"), format!("{pk2}"), "Display should reflect the actual key bytes"); +} + +#[test] +fn private_key_rejects_zero() { + match ECDSASecp256K1PrivateKey::from_bytes(&[0u8; 32]) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("expected DecodingError, got {other:?}"), + } +} + +#[test] +fn keygen_is_deterministic_given_a_deterministic_rng() { + // A fixed-output RNG (all-0x42 bytes) must yield the same key pair both times: pins that + // `keygen` has no hidden extra randomness source of its own. + struct FixedRng; + impl RNG for FixedRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0x42424242) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0x42u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0x42); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::_256bit + } + } + + let (pk1, sk1) = bouncycastle_ecdsa::keys_p256k1::keygen(&mut FixedRng).unwrap(); + let (pk2, sk2) = bouncycastle_ecdsa::keys_p256k1::keygen(&mut FixedRng).unwrap(); + assert_eq!(sk1, sk2); + assert_eq!(pk1, pk2); +} + +/// FIPS 186-5 Appendix A.2.1 step 3: an RNG weaker than the 128-bit security strength +/// secp256k1's order calls for (SP 800-57 Part 1 Rev. 5, Table 2) is refused rather than silently +/// used. +#[test] +fn keygen_rejects_an_rng_below_the_required_strength() { + struct WeakRng; + impl RNG for WeakRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::None + } + } + + assert!(matches!( + bouncycastle_ecdsa::keys_p256k1::keygen(&mut WeakRng), + Err(SignatureError::RNGError( + bouncycastle_core::errors::RNGError::SecurityStrengthInsufficientForAlgorithm + )) + )); +} + +/// FIPS 186-5 §6.2 puts `d` in `[1, n-1]`, so an encoding of `n` or anything above it is not a +/// key. Reducing such an encoding mod `n` instead of rejecting it would give a single key two (in +/// fact, unboundedly many) valid-looking encodings: `d = n + 1` would load as `d = 1`. +#[test] +fn private_key_rejects_values_at_or_above_the_group_order() { + let n = + bouncycastle_ec::p256k1_sec1::be_bytes_from_limbs(&bouncycastle_ec::p256k1_scalar::N_LIMBS); + let mut n_plus_1 = n; + n_plus_1[31] += 1; // n's last byte is well below 0xff, so this never carries + + for (name, bytes) in [("n", n), ("n + 1", n_plus_1), ("2^256 - 1", [0xffu8; 32])] { + match ECDSASecp256K1PrivateKey::from_bytes(&bytes) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("d = {name} should have been rejected, got {other:?}"), + } + } + + // n - 1 is the largest valid d and must still load, and must not collide with any of the above. + let mut n_minus_1 = n; + n_minus_1[31] -= 1; // n is odd, so this never borrows + let sk = ECDSASecp256K1PrivateKey::from_bytes(&n_minus_1).expect("d = n - 1 is in range"); + assert_eq!(sk.encode(), n_minus_1, "d = n - 1 must round-trip unchanged"); +} + +/// The raw encoding is exactly `r || s`; anything longer or shorter is a different, malformed +/// encoding, not a signature in a roomy buffer. Accepting trailing bytes would turn one valid +/// signature into unlimited distinct byte strings that all verify. +#[test] +fn verify_rejects_signature_of_the_wrong_length() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"length-checked signature"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + for extra in 1..=3 { + let mut too_long = sig.to_vec(); + too_long.extend(core::iter::repeat_n(0xAAu8, extra)); + match pk.verify(msg, None, &too_long) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{extra} trailing byte(s) should have failed, got {other:?}"), + } + } + + for short in 1..=3 { + match pk.verify(msg, None, &sig[..64 - short]) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{short} byte(s) short should have failed, got {other:?}"), + } + } +} + +/// FIPS 186-5 §6.4.2 step 1: `r` and `s` must each be in `[1, n-1]`, and an out-of-range value is +/// rejected rather than reduced. Each half of a valid signature is replaced in turn by `0`, by +/// `n`, and by the all-ones pattern; the untouched half stays valid, so only the range check can +/// be what rejects the result. The wycheproof suites cover this too, but only for the curves +/// and encodings they happen to include, and not by name. +#[test] +fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"range-checked r and s"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + let n = + bouncycastle_ec::p256k1_sec1::be_bytes_from_limbs(&bouncycastle_ec::p256k1_scalar::N_LIMBS); + for (name, bad) in [("0", [0u8; 32]), ("n", n), ("2^256 - 1", [0xffu8; 32])] { + for (half, offset) in [("r", 0usize), ("s", 32usize)] { + let mut tampered = sig; + tampered[offset..offset + 32].copy_from_slice(&bad); + match pk.verify(msg, None, &tampered) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("{half} = {name} should have been rejected, got {other:?}"), + } + } + } +} diff --git a/crypto/ecdsa/tests/ecdsa_p384_tests.rs b/crypto/ecdsa/tests/ecdsa_p384_tests.rs new file mode 100644 index 000000000..a4651694a --- /dev/null +++ b/crypto/ecdsa/tests/ecdsa_p384_tests.rs @@ -0,0 +1,339 @@ +//! Behavioural tests for [`ECDSAP384`], mirroring `ecdsa_p256_tests.rs`. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_core_test_framework::signature::{ + TestFrameworkSignature, TestFrameworkSignatureKeys, +}; +use bouncycastle_ecdsa::ecdsa_p384::ECDSAP384; +use bouncycastle_ecdsa::keys_common::DerivePublicKey; +use bouncycastle_ecdsa::keys_p384::{ECDSAP384PrivateKey, ECDSAP384PublicKey, keygen}; +use bouncycastle_rng::DefaultRNG; + +#[test] +fn derive_pk_matches_keygen() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_eq!(sk.derive_pk(), pk); +} + +/// [`ECDSAP384`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. +#[test] +fn conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework.test_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDSAP384`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework + .test_deterministic_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let deterministic = sk.sign_deterministic(msg, None).unwrap(); + pk.verify(msg, None, &random).unwrap(); + pk.verify(msg, None, &deterministic).unwrap(); +} + +#[test] +fn key_boundary_conditions() { + TestFrameworkSignatureKeys::new().test_keys::( + || keygen(&mut DefaultRNG::default()), + ); +} + +#[test] +fn sign_rng_produces_distinct_valid_signatures() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"randomised ECDSA signing"; + + let sig1 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let sig2 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); + + pk.verify(msg, None, &sig1).unwrap(); + pk.verify(msg, None, &sig2).unwrap(); +} + +#[test] +fn sign_rng_signature_rejected_under_wrong_key() { + let (_, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let (other_pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + match other_pk.verify(msg, None, &sig) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("expected SignatureVerificationFailed, got {other:?}"), + } +} + +#[test] +fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing"; + + let raw = sk.sign_deterministic(msg, None).unwrap(); + let (der, der_len) = sk.sign_der_deterministic(msg, None).unwrap(); + + let mut r_out = [0u8; 48]; + let mut s_out = [0u8; 48]; + bouncycastle_ecdsa::der::decode(&der[..der_len], &mut r_out, &mut s_out) + .expect("sign_der's output must be a well-formed DER SEQUENCE { r, s }"); + assert_eq!(r_out, raw[..48], "DER-decoded r must match sign's raw r"); + assert_eq!(s_out, raw[48..], "DER-decoded s must match sign's raw s"); + + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + + let mut tampered = der; + tampered[der_len - 1] ^= 0xFF; + assert!( + pk.verify_der(msg, None, &tampered[..der_len]).is_err(), + "a corrupted DER signature must not verify" + ); +} + +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = sk.sign_der(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); +} + +#[test] +fn public_key_uncompressed_and_compressed_round_trip() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let uncompressed = pk.encode(); + assert_eq!(uncompressed[0], 0x04); + assert_eq!(ECDSAP384PublicKey::from_bytes(&uncompressed).unwrap(), pk); + + let compressed_tag = if uncompressed[96] & 1 == 0 { 0x02 } else { 0x03 }; + let mut compressed = [0u8; 49]; + compressed[0] = compressed_tag; + compressed[1..].copy_from_slice(&uncompressed[1..49]); + assert_eq!(ECDSAP384PublicKey::from_bytes(&compressed).unwrap(), pk); +} + +#[test] +fn private_key_rejects_zero() { + match ECDSAP384PrivateKey::from_bytes(&[0u8; 48]) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("expected DecodingError, got {other:?}"), + } +} + +#[test] +fn private_key_encode_round_trips_exact_bytes() { + let mut bytes = [0u8; 48]; + bytes[47] = 0x2A; // d = 42 + let sk = ECDSAP384PrivateKey::from_bytes(&bytes).unwrap(); + assert_eq!(sk.encode(), bytes); + + let mut out = [0xFFu8; 48]; + let written = sk.encode_out(&mut out); + assert_eq!(written, 48); + assert_eq!(out, bytes); +} + +#[test] +fn public_key_encode_out_matches_encode() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let encoded = pk.encode(); + + let mut out = [0xFFu8; 97]; + let written = pk.encode_out(&mut out); + assert_eq!(written, 97); + assert_eq!(out, encoded); +} + +#[test] +fn public_key_display_differs_for_different_keys() { + let (pk1, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let (pk2, _) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_ne!(format!("{pk1}"), format!("{pk2}")); +} + +#[test] +fn keygen_is_deterministic_given_a_deterministic_rng() { + struct FixedRng; + impl RNG for FixedRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0x42424242) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0x42u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0x42); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::_256bit + } + } + + let (pk1, sk1) = bouncycastle_ecdsa::keys_p384::keygen(&mut FixedRng).unwrap(); + let (pk2, sk2) = bouncycastle_ecdsa::keys_p384::keygen(&mut FixedRng).unwrap(); + assert_eq!(sk1, sk2); + assert_eq!(pk1, pk2); +} + +/// FIPS 186-5 Appendix A.2.1 step 3: an RNG weaker than the 192-bit security strength +/// P-384's order calls for (SP 800-57 Part 1 Rev. 5, Table 2) is refused rather than silently +/// used. +#[test] +fn keygen_rejects_an_rng_below_the_required_strength() { + struct WeakRng; + impl RNG for WeakRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::None + } + } + + assert!(matches!( + bouncycastle_ecdsa::keys_p384::keygen(&mut WeakRng), + Err(SignatureError::RNGError( + bouncycastle_core::errors::RNGError::SecurityStrengthInsufficientForAlgorithm + )) + )); +} + +/// FIPS 186-5 §6.2 puts `d` in `[1, n-1]`, so an encoding of `n` or anything above it is not a +/// key. Reducing such an encoding mod `n` instead of rejecting it would give a single key two (in +/// fact, unboundedly many) valid-looking encodings: `d = n + 1` would load as `d = 1`. +#[test] +fn private_key_rejects_values_at_or_above_the_group_order() { + let n = bouncycastle_ec::p384_sec1::be_bytes_from_limbs(&bouncycastle_ec::p384_scalar::N_LIMBS); + let mut n_plus_1 = n; + n_plus_1[47] += 1; // n's last byte is well below 0xff, so this never carries + + for (name, bytes) in [("n", n), ("n + 1", n_plus_1), ("2^384 - 1", [0xffu8; 48])] { + match ECDSAP384PrivateKey::from_bytes(&bytes) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("d = {name} should have been rejected, got {other:?}"), + } + } + + // n - 1 is the largest valid d and must still load, and must not collide with any of the above. + let mut n_minus_1 = n; + n_minus_1[47] -= 1; // n is odd, so this never borrows + let sk = ECDSAP384PrivateKey::from_bytes(&n_minus_1).expect("d = n - 1 is in range"); + assert_eq!(sk.encode(), n_minus_1, "d = n - 1 must round-trip unchanged"); +} + +/// The raw encoding is exactly `r || s`; anything longer or shorter is a different, malformed +/// encoding, not a signature in a roomy buffer. Accepting trailing bytes would turn one valid +/// signature into unlimited distinct byte strings that all verify. +#[test] +fn verify_rejects_signature_of_the_wrong_length() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"length-checked signature"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + for extra in 1..=3 { + let mut too_long = sig.to_vec(); + too_long.extend(core::iter::repeat_n(0xAAu8, extra)); + match pk.verify(msg, None, &too_long) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{extra} trailing byte(s) should have failed, got {other:?}"), + } + } + + for short in 1..=3 { + match pk.verify(msg, None, &sig[..96 - short]) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{short} byte(s) short should have failed, got {other:?}"), + } + } +} + +/// FIPS 186-5 §6.4.2 step 1: `r` and `s` must each be in `[1, n-1]`, and an out-of-range value is +/// rejected rather than reduced. Each half of a valid signature is replaced in turn by `0`, by +/// `n`, and by the all-ones pattern; the untouched half stays valid, so only the range check can +/// be what rejects the result. The wycheproof suites cover this too, but only for the curves +/// and encodings they happen to include, and not by name. +#[test] +fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"range-checked r and s"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + let n = bouncycastle_ec::p384_sec1::be_bytes_from_limbs(&bouncycastle_ec::p384_scalar::N_LIMBS); + for (name, bad) in [("0", [0u8; 48]), ("n", n), ("2^384 - 1", [0xffu8; 48])] { + for (half, offset) in [("r", 0usize), ("s", 48usize)] { + let mut tampered = sig; + tampered[offset..offset + 48].copy_from_slice(&bad); + match pk.verify(msg, None, &tampered) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("{half} = {name} should have been rejected, got {other:?}"), + } + } + } +} diff --git a/crypto/ecdsa/tests/ecdsa_p521_tests.rs b/crypto/ecdsa/tests/ecdsa_p521_tests.rs new file mode 100644 index 000000000..892466d92 --- /dev/null +++ b/crypto/ecdsa/tests/ecdsa_p521_tests.rs @@ -0,0 +1,340 @@ +//! Behavioural tests for [`ECDSAP521`], mirroring `ecdsa_p256_tests.rs`. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_core_test_framework::signature::{ + TestFrameworkSignature, TestFrameworkSignatureKeys, +}; +use bouncycastle_ecdsa::ecdsa_p521::ECDSAP521; +use bouncycastle_ecdsa::keys_common::DerivePublicKey; +use bouncycastle_ecdsa::keys_p521::{ECDSAP521PrivateKey, ECDSAP521PublicKey, keygen}; +use bouncycastle_rng::DefaultRNG; + +#[test] +fn derive_pk_matches_keygen() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_eq!(sk.derive_pk(), pk); +} + +/// [`ECDSAP521`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. +#[test] +fn conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework.test_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDSAP521`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework + .test_deterministic_signature::( + || keygen(&mut DefaultRNG::default()), + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let deterministic = sk.sign_deterministic(msg, None).unwrap(); + pk.verify(msg, None, &random).unwrap(); + pk.verify(msg, None, &deterministic).unwrap(); +} + +#[test] +fn key_boundary_conditions() { + TestFrameworkSignatureKeys::new() + .test_keys::(|| { + keygen(&mut DefaultRNG::default()) + }); +} + +#[test] +fn sign_rng_produces_distinct_valid_signatures() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"randomised ECDSA signing"; + + let sig1 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + let sig2 = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); + + pk.verify(msg, None, &sig1).unwrap(); + pk.verify(msg, None, &sig2).unwrap(); +} + +#[test] +fn sign_rng_signature_rejected_under_wrong_key() { + let (_, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let (other_pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + match other_pk.verify(msg, None, &sig) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("expected SignatureVerificationFailed, got {other:?}"), + } +} + +#[test] +fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing"; + + let raw = sk.sign_deterministic(msg, None).unwrap(); + let (der, der_len) = sk.sign_der_deterministic(msg, None).unwrap(); + + let mut r_out = [0u8; 66]; + let mut s_out = [0u8; 66]; + bouncycastle_ecdsa::der::decode(&der[..der_len], &mut r_out, &mut s_out) + .expect("sign_der's output must be a well-formed DER SEQUENCE { r, s }"); + assert_eq!(r_out, raw[..66], "DER-decoded r must match sign's raw r"); + assert_eq!(s_out, raw[66..], "DER-decoded s must match sign's raw s"); + + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + + let mut tampered = der; + tampered[der_len - 1] ^= 0xFF; + assert!( + pk.verify_der(msg, None, &tampered[..der_len]).is_err(), + "a corrupted DER signature must not verify" + ); +} + +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = sk.sign_der(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); + pk.verify_der(msg, None, &der[..der_len]).unwrap(); +} + +#[test] +fn public_key_uncompressed_and_compressed_round_trip() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let uncompressed = pk.encode(); + assert_eq!(uncompressed[0], 0x04); + assert_eq!(ECDSAP521PublicKey::from_bytes(&uncompressed).unwrap(), pk); + + let compressed_tag = if uncompressed[132] & 1 == 0 { 0x02 } else { 0x03 }; + let mut compressed = [0u8; 67]; + compressed[0] = compressed_tag; + compressed[1..].copy_from_slice(&uncompressed[1..67]); + assert_eq!(ECDSAP521PublicKey::from_bytes(&compressed).unwrap(), pk); +} + +#[test] +fn private_key_rejects_zero() { + match ECDSAP521PrivateKey::from_bytes(&[0u8; 66]) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("expected DecodingError, got {other:?}"), + } +} + +#[test] +fn private_key_encode_round_trips_exact_bytes() { + let mut bytes = [0u8; 66]; + bytes[65] = 0x2A; // d = 42 + let sk = ECDSAP521PrivateKey::from_bytes(&bytes).unwrap(); + assert_eq!(sk.encode(), bytes); + + let mut out = [0xFFu8; 66]; + let written = sk.encode_out(&mut out); + assert_eq!(written, 66); + assert_eq!(out, bytes); +} + +#[test] +fn public_key_encode_out_matches_encode() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let encoded = pk.encode(); + + let mut out = [0xFFu8; 133]; + let written = pk.encode_out(&mut out); + assert_eq!(written, 133); + assert_eq!(out, encoded); +} + +#[test] +fn public_key_display_differs_for_different_keys() { + let (pk1, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let (pk2, _) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_ne!(format!("{pk1}"), format!("{pk2}")); +} + +#[test] +fn keygen_is_deterministic_given_a_deterministic_rng() { + struct FixedRng; + impl RNG for FixedRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0x42424242) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0x42u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0x42); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::_256bit + } + } + + let (pk1, sk1) = bouncycastle_ecdsa::keys_p521::keygen(&mut FixedRng).unwrap(); + let (pk2, sk2) = bouncycastle_ecdsa::keys_p521::keygen(&mut FixedRng).unwrap(); + assert_eq!(sk1, sk2); + assert_eq!(pk1, pk2); +} + +/// FIPS 186-5 Appendix A.2.1 step 3: an RNG weaker than the 256-bit security strength +/// P-521's order calls for (SP 800-57 Part 1 Rev. 5, Table 2) is refused rather than silently +/// used. +#[test] +fn keygen_rejects_an_rng_below_the_required_strength() { + struct WeakRng; + impl RNG for WeakRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::None + } + } + + assert!(matches!( + bouncycastle_ecdsa::keys_p521::keygen(&mut WeakRng), + Err(SignatureError::RNGError( + bouncycastle_core::errors::RNGError::SecurityStrengthInsufficientForAlgorithm + )) + )); +} + +/// FIPS 186-5 §6.2 puts `d` in `[1, n-1]`, so an encoding of `n` or anything above it is not a +/// key. Reducing such an encoding mod `n` instead of rejecting it would give a single key two (in +/// fact, unboundedly many) valid-looking encodings: `d = n + 1` would load as `d = 1`. +#[test] +fn private_key_rejects_values_at_or_above_the_group_order() { + let n = bouncycastle_ec::p521_sec1::be_bytes_from_limbs(&bouncycastle_ec::p521_scalar::N_LIMBS); + let mut n_plus_1 = n; + n_plus_1[65] += 1; // n's last byte is well below 0xff, so this never carries + + for (name, bytes) in [("n", n), ("n + 1", n_plus_1), ("2^528 - 1", [0xffu8; 66])] { + match ECDSAP521PrivateKey::from_bytes(&bytes) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("d = {name} should have been rejected, got {other:?}"), + } + } + + // n - 1 is the largest valid d and must still load, and must not collide with any of the above. + let mut n_minus_1 = n; + n_minus_1[65] -= 1; // n is odd, so this never borrows + let sk = ECDSAP521PrivateKey::from_bytes(&n_minus_1).expect("d = n - 1 is in range"); + assert_eq!(sk.encode(), n_minus_1, "d = n - 1 must round-trip unchanged"); +} + +/// The raw encoding is exactly `r || s`; anything longer or shorter is a different, malformed +/// encoding, not a signature in a roomy buffer. Accepting trailing bytes would turn one valid +/// signature into unlimited distinct byte strings that all verify. +#[test] +fn verify_rejects_signature_of_the_wrong_length() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"length-checked signature"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + for extra in 1..=3 { + let mut too_long = sig.to_vec(); + too_long.extend(core::iter::repeat_n(0xAAu8, extra)); + match pk.verify(msg, None, &too_long) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{extra} trailing byte(s) should have failed, got {other:?}"), + } + } + + for short in 1..=3 { + match pk.verify(msg, None, &sig[..132 - short]) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{short} byte(s) short should have failed, got {other:?}"), + } + } +} + +/// FIPS 186-5 §6.4.2 step 1: `r` and `s` must each be in `[1, n-1]`, and an out-of-range value is +/// rejected rather than reduced. Each half of a valid signature is replaced in turn by `0`, by +/// `n`, and by the all-ones pattern; the untouched half stays valid, so only the range check can +/// be what rejects the result. The wycheproof suites cover this too, but only for the curves +/// and encodings they happen to include, and not by name. +#[test] +fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"range-checked r and s"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, None).unwrap(); + pk.verify(msg, None, &sig).expect("the untampered signature must verify"); + + let n = bouncycastle_ec::p521_sec1::be_bytes_from_limbs(&bouncycastle_ec::p521_scalar::N_LIMBS); + for (name, bad) in [("0", [0u8; 66]), ("n", n), ("2^528 - 1", [0xffu8; 66])] { + for (half, offset) in [("r", 0usize), ("s", 66usize)] { + let mut tampered = sig; + tampered[offset..offset + 66].copy_from_slice(&bad); + match pk.verify(msg, None, &tampered) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("{half} = {name} should have been rejected, got {other:?}"), + } + } + } +} diff --git a/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs new file mode 100644 index 000000000..0e509eb0c --- /dev/null +++ b/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs @@ -0,0 +1,167 @@ +//! Cross-check against an independent Python implementation of RFC 6979 §3.2 (using Python's own +//! `hmac`/`hashlib`, not derived from this crate) for brainpoolP256r1/SHA-256: RFC 6979 has no +//! official Appendix A.2.x vectors for brainpool curves (its appendix only covers the NIST curves +//! P-192 through P-521), so this is the closest equivalent -- five independently-generated `(d, +//! msg)` pairs (empty, short ASCII, longer ASCII, all-zero, all-`0xff`), each with the +//! deterministic `k`, the resulting `(r, s)`, and `Q = [d]G` computed via the standard affine +//! group law (general `a`), `random.seed 1234567`. +//! +//! On this curve the §3.2 step h.3 rejection loop runs on a third of real messages (see +//! `rfc6979_bp256r1`'s module docs), and these vectors pin it deliberately: CASE_0 rejects one +//! candidate before its `k`, and a sixth pair, CASE_5, is chosen so that five in a row are +//! rejected, exercising the `K = HMAC_K(V || 0x00)` re-key five times. Every vector, including +//! both rejecting ones, is additionally reproduced by python-ecdsa 0.19.2 (its own pure-Python +//! RFC 6979) and by OpenSSL 4.0.2's deterministic ECDSA (through the cryptography 50.0.1 +//! bindings), which also anchors the curve constants a second way. + +use bouncycastle_core::signature_key::{DeterministicSigningKey, VerifyingKey}; +use bouncycastle_core::traits::HashOneShot; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::bp256r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp256r1_scalar::Bp256r1Scalar; +use bouncycastle_ecdsa::keys_bp256r1::{ECDSABp256r1PrivateKey, ECDSABp256r1PublicKey}; +use bouncycastle_ecdsa::rfc6979_bp256r1::generate_k; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_sha2::SHA256; + +struct Vector { + d: &'static str, + msg: &'static [u8], + k: &'static str, + r: &'static str, + s: &'static str, + ux: &'static str, + uy: &'static str, +} + +// The first h.3 candidate for this (d, msg) is >= n: the accepted k is the second, after one +// re-key of the HMAC chain. +const CASE_0: Vector = Vector { + d: "1201AF823A0A4FE05170CFF294DC13FC115AC6CB3C58FD846EC5C07FF6908E54", + msg: b"", + k: "A0E19EABA085BB08D16F30C7AE971295A949C8B000453F712784363710FDC060", + r: "0B8F953A8F0D6D91DAB6A4A6457FA641FDD10C96E94086CCB62D8513F24D69C7", + s: "60819F11F5A7D66CF4E6F762682C9DF73013B97343260D38E25E66ABCBF1448C", + ux: "A6A0C25545FABE99A6DA940773677938492C2763EC54A5146654D5E4BCA7685D", + uy: "41BA5253532155D7F7F80D8103CA988A60D17407E97D7A778A737535ADFF0773", +}; + +const CASE_1: Vector = Vector { + d: "7E7B91BCA9655DC7AA94AFFD753150AEAF395DDF588590D1CB31A5A541346EC1", + msg: b"sample", + k: "7D360D0B1F5B42A6B50D004C76B5715BDD3DFD4CE084CBC77CEDA0014C4F750A", + r: "116AD2387430F54440072A9403C76E9AFA5525A3268174A55CB246E94782A595", + s: "2F09D7DD0AFEF60D74CBD16CC7DD19D8DCBB2EDEF81B7A11A21965C46193CBB2", + ux: "4ECD080482532BFC1F6C5DDFBE0F445BCBC3C40E64794FF51ADF8E3205145F04", + uy: "0AE02177241814BF08BC717B94C73ECC26849951F3F22DF8C29DD52B1781B41C", +}; + +const CASE_2: Vector = Vector { + d: "31A01101800F36D7D7F978F0A674894EAF61ACFE376CEAE9AE8692ADE621E465", + msg: b"test message for RFC 6979 brainpoolP256r1 cross-check", + k: "2B7CFCFDB9BD902E6CB97EEEE4B66DF58C299A876CA17EDDF07DF61A7C15B471", + r: "A374D349024E2B2829ECC45A3D7882F28A1CB70A99151CEBDAC98AF701A84A1E", + s: "89968BA364EE210AA894C67C848B67C78162DA25F20FDFC3F883088169FF90D4", + ux: "845B89330FE2D4A0DD98C53051E662897C6DEAA47DC984DFFD3681437891CCE6", + uy: "1BFD2C17D33734CA963DA3A21C8FB2BB02CC8412B860F2CB75B1C04F538C5288", +}; + +const CASE_3: Vector = Vector { + d: "330EC367DE3D130E41B64A8D9CA805E18BE822EDD22F87F9914BFAF280DD7C16", + msg: &[0u8; 32], + k: "08C64809010AFC34B047B996465699C164FBA094BB1A36ABA4A23D3362DFEC98", + r: "55558146F7D5CF164C97475C004BE0C2A4E6EB69EA0F140239D360869B88DBD5", + s: "87F099C25E243DA8B4C0162D2729169A681962DE7FA3AA078463B733A518D729", + ux: "7AE313447CF7324732217CFA91F5E11A4CB24449C45BCE6642925A5E238CA222", + uy: "108EC1D7914D723E30D1CA6F65756350476C31703D6D3F07FC848F98BD325906", +}; + +const CASE_4: Vector = Vector { + d: "A67E90EC30BC65D57594E68E791FA9ADF8E3DD706ECB52454C5082AF09E88A09", + msg: &[0xffu8; 10], + k: "65C065EB328E8D876D31F307C44647ABCFE66291FD28244A9024B38D3E1B482E", + r: "388B00024108BE9D41B90930E18AE68F05C3CC6F3E318410E3887DD4BE841F5C", + s: "216DCA2EB91D9D919EE961A5A9C840DDFA64D06CC52815C72D2740E2581CF60B", + ux: "767C8507AA96B62B2028B9203DD7554274D10444AB32769666695FBADFFF26AB", + uy: "62F10FDBD0CDE940DD72CEBD0A3C8FB1D734ECB5C6B287CDEB2E50CA650D67C2", +}; + +// Pins the h.3 continue-chain deliberately: five candidates in a row are >= n, so the accepted k +// follows five `K = HMAC_K(V || 0x00)` re-keys. d is SHA-512("bc-rust brainpoolP256r1 h.3 +// rejection pin") reduced into [1, n-1]; the message index is the first to reach five rejections. +const CASE_5: Vector = Vector { + d: "07C1C10F28FB620F5ABFC2231E5CF77931E282BFCC735F96DE13FDCCFB51F95E", + msg: b"brainpoolP256r1 rejection test 363", + k: "753584443CB1744D143C4E19D1DE051F2DB89DEA3F208C1089EFAF093C459CA9", + r: "2228C1F981B3F9514F15D1D2CFFCF96E5F6C93ABF5208872611B5A69E3B6CC2D", + s: "64B2088C315AB6158001B09B6F15EC2FE314F1B5CA971E92453A1FCDF414D472", + ux: "8CBDEA6139F062041BDF604D0C80750AC944D23BFD5406A1EFA70A5FC19B290C", + uy: "46A4AE6AA2E4182CC865D49169994AD26DA55207D4E517DC68780EC25A263DC1", +}; + +const CASES: [&Vector; 6] = [&CASE_0, &CASE_1, &CASE_2, &CASE_3, &CASE_4, &CASE_5]; + +fn bytes32(hex: &str) -> [u8; 32] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +fn public_key_for(v: &Vector) -> ECDSABp256r1PublicKey { + let mut uncompressed = [0u8; 65]; + uncompressed[0] = 0x04; + uncompressed[1..33].copy_from_slice(&bytes32(v.ux)); + uncompressed[33..65].copy_from_slice(&bytes32(v.uy)); + ECDSABp256r1PublicKey::from_bytes(&uncompressed).unwrap() +} + +#[test] +fn public_key_matches_vectors() { + for v in CASES { + let d = Bp256r1Scalar::from_be_bytes(&bytes32(v.d)); + let q = comb_multiply_base_point(&d); + let (x, y) = q.to_affine().unwrap(); + assert_eq!( + x.to_limbs(), + bouncycastle_ec::bp256r1_sec1::limbs_from_be_bytes(&bytes32(v.ux)), + "Qx for d = {}", + v.d + ); + assert_eq!( + y.to_limbs(), + bouncycastle_ec::bp256r1_sec1::limbs_from_be_bytes(&bytes32(v.uy)), + "Qy for d = {}", + v.d + ); + } +} + +#[test] +fn generate_k_matches_vectors() { + for v in CASES { + let d = Bp256r1Scalar::from_be_bytes(&bytes32(v.d)); + let h: [u8; 32] = SHA256::hash(v.msg)[..32].try_into().unwrap(); + let k = generate_k(&d, &h); + assert_eq!(k, Bp256r1Scalar::from_be_bytes(&bytes32(v.k)), "k for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_sign_matches_vectors() { + for v in CASES { + let sk = ECDSABp256r1PrivateKey::from_bytes(&bytes32(v.d)).unwrap(); + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + let mut expected = [0u8; 64]; + expected[..32].copy_from_slice(&bytes32(v.r)); + expected[32..].copy_from_slice(&bytes32(v.s)); + assert_eq!(sig, expected, "signature for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_signature_verifies() { + for v in CASES { + let sk = ECDSABp256r1PrivateKey::from_bytes(&bytes32(v.d)).unwrap(); + let pk = public_key_for(v); + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + pk.verify(v.msg, None, &sig).unwrap(); + } +} diff --git a/crypto/ecdsa/tests/rfc6979_bp384r1_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_bp384r1_vectors_tests.rs new file mode 100644 index 000000000..35bf752bb --- /dev/null +++ b/crypto/ecdsa/tests/rfc6979_bp384r1_vectors_tests.rs @@ -0,0 +1,166 @@ +//! Cross-check against an independent Python implementation of RFC 6979 §3.2 (using Python's own +//! `hmac`/`hashlib`, not derived from this crate) for brainpoolP384r1/SHA-384: RFC 6979 has no +//! official Appendix A.2.x vectors for brainpool curves; five independently-generated `(d, msg)` +//! pairs (empty, short ASCII, longer ASCII, all-zero, all-`0xff`), each with the deterministic +//! `k`, the resulting `(r, s)`, and `Q = [d]G` computed via the standard affine group law (general +//! `a`), `random.seed 1234567`. +//! +//! On this curve the §3.2 step h.3 rejection loop runs on nearly half of real messages (see +//! `rfc6979_bp384r1`'s module docs), and these vectors pin it deliberately: CASE_0 rejects two +//! candidates before its `k`, CASE_4 one, and a sixth pair, CASE_5, is chosen so that six in a +//! row are rejected, exercising the `K = HMAC_K(V || 0x00)` re-key six times. Every vector, +//! including all three rejecting ones, is additionally reproduced by python-ecdsa 0.19.2 (its own +//! pure-Python RFC 6979) and by OpenSSL 4.0.2's deterministic ECDSA (through the cryptography +//! 50.0.1 bindings), which also anchors the curve constants a second way. + +use bouncycastle_core::signature_key::{DeterministicSigningKey, VerifyingKey}; +use bouncycastle_core::traits::HashOneShot; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::bp384r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp384r1_scalar::Bp384r1Scalar; +use bouncycastle_ecdsa::keys_bp384r1::{ECDSABp384r1PrivateKey, ECDSABp384r1PublicKey}; +use bouncycastle_ecdsa::rfc6979_bp384r1::generate_k; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_sha2::SHA384; + +struct Vector { + d: &'static str, + msg: &'static [u8], + k: &'static str, + r: &'static str, + s: &'static str, + ux: &'static str, + uy: &'static str, +} + +// The first two h.3 candidates for this (d, msg) are >= n: the accepted k is the third, after +// two re-keys of the HMAC chain. +const CASE_0: Vector = Vector { + d: "0A3ECED49C8BE3E693AF8A68F77282BD1201AF823A0A4FE05170CFF294DC13FC115AC6CB3C58FD846EC5C07FF6908E54", + msg: b"", + k: "3FC56705D95B3FEE45DD25E9B16F5A1BCB7769D0AA8782A1F6A40ECC1913ABB96F1868AD0EE9391884B159B491B4FF53", + r: "26B68E91F47AF625048D8B83AAE51A225E8EF081DB72DB2DE9794BF363C0FC8057319941DD127664E7876D4013A6148F", + s: "2E86B247F68E070380656BAAFDEC7C4F14063241642A158FB6E002D7D86FA990574AB86A8C3CCE3B05D642DD35BE1787", + ux: "320C24C58062BC08BB59C576C8261D873B1BC8D2B7458A85C3734565B8D9CA289692E2AFE3633EB1C3A2D900813AEAA6", + uy: "503CA7DC163064CBFA64790AAFBE97D325B874DAB95D23AFA699CC00B3B8BB54E9BCC637713AF9E829D87878D27FA8A2", +}; + +const CASE_1: Vector = Vector { + d: "8BE822EDD22F87F9914BFAF280DD7C1531A01101800F36D7D7F978F0A674894EAF61ACFE376CEAE9AE8692ADE621E465", + msg: b"sample", + k: "3C7FA6ED4AAF3AAD9E43F4CE4358455015E8E9B33E169BFB59CCEF92DA47D304A238B94617732DC27BA29E14E3C296AE", + r: "0E3EB847856EDDBBA79B6A794E7C47CF2017FB55F82D12CEC8E8205CFB51B7F2925906CBD527AC1DD9283167E7E5D098", + s: "6F2E40C4E636F0FC6A55DC74AADC68D2C1E264C27948FA3BAE84943A67CC231AA7B0B0F3CC83C2C26BE2B0BF79415D10", + ux: "758855E97E1648F5FA81A6F3A4B987C1E043F428502954D30DB662E25BEC4A228E29CA78370F5B2EE1AC2DB4A853C157", + uy: "4AD9E8AECBB17CD175381EBF72F9933D9CDB9CDEF87641A480AF3A76225BDF38C87170F0CFAF6D1F1BC1E57FBAD061A1", +}; + +const CASE_2: Vector = Vector { + d: "59D4756A0A10302D86464ED67338BD3217BAA6642A507BD5336ED46492D516FEE5A381A0C9D32A03D4CCAF167412C30D", + msg: b"test message for RFC 6979 brainpoolP384r1 cross-check", + k: "16C5B418941903EEB9BCB368EF69803B46147902C83FE09972FFFC7D6C4C34F91B88A752CB31461A3B32D938ED365D31", + r: "39FD12109FD43E281557AA9A72C53644878349ACFD4014C09952AD23C7646D9FE38EB91AF48E3FE9E56BBE1C1B26C0E9", + s: "69AE82BD7A4DA2CD3486309D7544A665478EF93DC6B93A93611BCED16513AAC25AF1B79B99C1A3D2FCA7E11523640DA0", + ux: "7831F05270F3A9B5EBFFF2003DD13C3CDDCB67348265B73AF054F9AD46FC4D7EB134AAED3E78F1F126C9D6B0AC8CE09D", + uy: "78795C35667B76A614646A8F7187B29153A13DDF2CFABB29FCAC838103FD649A84335F4E3A5C6B915DF7F8189A2E496A", +}; + +const CASE_3: Vector = Vector { + d: "3BFDEAFDE8ED1C92C83FC7A3FB856FE01E484B7A312A44DD6494CF57049E427467DFEBC315C77C8AF429FCF59430B282", + msg: &[0u8; 32], + k: "80B3FC797B1EEB3B7B012691E4BF25B5A139C609A8F67A6FAAFB3E7DCE8F6B2ECCE425A4A05511D8ECD61D6A9AADF7D2", + r: "3149C49F6FF4515C00EDB3592F1139374B107E0592121AAFA3D4C159C04F00759BD648BD27B2BCDF737FC20D734C4862", + s: "80AF76FE73606B75DDE71E8FFA388BBE4FAEDB122D583DFB3571004C137BCAE440FA197D9FF91A0A351103A0D2956ACC", + ux: "239CF75A0C6291C46835846A3D634EB8682E9D70D443358F9D6EB7BBA08E5AA8B63937F56A7423BE76F566335B1E7DFE", + uy: "776FB4EA44FE91B2CD416CA75D3F115D0B044826D949E129F7A5B5008C6CB069925D86B6EEBD5EDCF813AC2B5D417305", +}; + +// One h.3 rejection: the accepted k is the second candidate. +const CASE_4: Vector = Vector { + d: "3AC2CE16303249B912258C63556A44E17CEE4B4D567578F26A89C3FC0098EFE577CBF8FCAC22AAE164F74D7169CEB006", + msg: &[0xffu8; 10], + k: "866B6A38D026D864CD5B3FF749B271D081AF3E6C3B53ACE0899006B722E30781C7258A734209E1C1C88451D73F431E29", + r: "41801494B55EE535AB19F416992D044A6E7F9272BACB9C898297F580BA8D23F261CD75936A2B283DFBD4C694EFEE4A42", + s: "0A13D21EBBB985D37F28921C8DA8DF207E8D7914062FCC4376CEEB85338BE6C661EBA26615CA8B3FF2CB620EFED64E44", + ux: "2123645AD346A966820312FC832DBAD86A20C249819121BCADBD17C1A18DE49605EBF74AEAA128C6E03F4CB48744983E", + uy: "64DE68D75FA78C5E6F9B37E297163E77EFFABB263D957D5C017EAF2AD4F9F9567664BB1E701959E6302BE803300CE40F", +}; + +// Pins the h.3 continue-chain deliberately: six candidates in a row are >= n, so the accepted k +// follows six `K = HMAC_K(V || 0x00)` re-keys -- the (d, msg) pair was searched for exactly that. +const CASE_5: Vector = Vector { + d: "65410D0A7B55AC6BE4151D747AF74AF138BC0BBDEE4345D0551AA055D0F995E9FAAA3692CCD92C41A178481E5EB2CB03", + msg: b"brainpoolP384r1 rejection test 3", + k: "1FD7200F56175EE6562B30503B474C69CAE25E83B924CCB619726AE707684E23820BEEB3BCCBBE9DFEA2FF805B26FB68", + r: "1AB76CFE4E93C696421D80EFC50C7F026B5C12125488970EFCF9FBA40D6734F2323F57BCE809923C886ADACFB51F105C", + s: "2B260A5C092C38E580E01EDE836CBA95D9C9C0F4A82CDE497B1603AFF0BE1E7141FE57177D940EE41F89A8F383C9D8C2", + ux: "87086428290CD23F878DE8250334D9A67EFCBF9DC90161B6A956CCF48AF45B3989FD7785A95B669080D6D3F7514E785B", + uy: "3E7F94B4B7826F07898649827C215A147C71A1DFDC0B42592998135F148B6EB53AB0E23C4DDF2D0BF795FB275B09A656", +}; + +const CASES: [&Vector; 6] = [&CASE_0, &CASE_1, &CASE_2, &CASE_3, &CASE_4, &CASE_5]; + +fn bytes48(hex: &str) -> [u8; 48] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +fn public_key_for(v: &Vector) -> ECDSABp384r1PublicKey { + let mut uncompressed = [0u8; 97]; + uncompressed[0] = 0x04; + uncompressed[1..49].copy_from_slice(&bytes48(v.ux)); + uncompressed[49..97].copy_from_slice(&bytes48(v.uy)); + ECDSABp384r1PublicKey::from_bytes(&uncompressed).unwrap() +} + +#[test] +fn public_key_matches_vectors() { + for v in CASES { + let d = Bp384r1Scalar::from_be_bytes(&bytes48(v.d)); + let q = comb_multiply_base_point(&d); + let (x, y) = q.to_affine().unwrap(); + assert_eq!( + x.to_limbs(), + bouncycastle_ec::bp384r1_sec1::limbs_from_be_bytes(&bytes48(v.ux)), + "Qx for d = {}", + v.d + ); + assert_eq!( + y.to_limbs(), + bouncycastle_ec::bp384r1_sec1::limbs_from_be_bytes(&bytes48(v.uy)), + "Qy for d = {}", + v.d + ); + } +} + +#[test] +fn generate_k_matches_vectors() { + for v in CASES { + let d = Bp384r1Scalar::from_be_bytes(&bytes48(v.d)); + let h: [u8; 48] = SHA384::hash(v.msg)[..48].try_into().unwrap(); + let k = generate_k(&d, &h); + assert_eq!(k, Bp384r1Scalar::from_be_bytes(&bytes48(v.k)), "k for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_sign_matches_vectors() { + for v in CASES { + let sk = ECDSABp384r1PrivateKey::from_bytes(&bytes48(v.d)).unwrap(); + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + let mut expected = [0u8; 96]; + expected[..48].copy_from_slice(&bytes48(v.r)); + expected[48..].copy_from_slice(&bytes48(v.s)); + assert_eq!(sig, expected, "signature for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_signature_verifies() { + for v in CASES { + let sk = ECDSABp384r1PrivateKey::from_bytes(&bytes48(v.d)).unwrap(); + let pk = public_key_for(v); + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + pk.verify(v.msg, None, &sig).unwrap(); + } +} diff --git a/crypto/ecdsa/tests/rfc6979_bp512r1_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_bp512r1_vectors_tests.rs new file mode 100644 index 000000000..680d5250f --- /dev/null +++ b/crypto/ecdsa/tests/rfc6979_bp512r1_vectors_tests.rs @@ -0,0 +1,165 @@ +//! Cross-check against an independent Python implementation of RFC 6979 §3.2 (using Python's own +//! `hmac`/`hashlib`, not derived from this crate) for brainpoolP512r1/SHA-512: RFC 6979 has no +//! official Appendix A.2.x vectors for brainpool curves; five independently-generated `(d, msg)` +//! pairs (empty, short ASCII, longer ASCII, all-zero, all-`0xff`), each with the deterministic +//! `k`, the resulting `(r, s)`, and `Q = [d]G` computed via the standard affine group law (general +//! `a`), `random.seed 1234567`. +//! +//! On this curve the §3.2 step h.3 rejection loop runs on a third of real messages (see +//! `rfc6979_bp512r1`'s module docs), and these vectors pin it deliberately: CASE_0 and CASE_1 +//! each reject one candidate before their `k`, and a sixth pair, CASE_5, is chosen so that five +//! in a row are rejected, exercising the `K = HMAC_K(V || 0x00)` re-key five times. Every vector, +//! including all three rejecting ones, is additionally reproduced by python-ecdsa 0.19.2 (its own +//! pure-Python RFC 6979) and by OpenSSL 4.0.2's deterministic ECDSA (through the cryptography +//! 50.0.1 bindings), which also anchors the curve constants a second way. + +use bouncycastle_core::signature_key::{DeterministicSigningKey, VerifyingKey}; +use bouncycastle_core::traits::HashOneShot; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::bp512r1_comb::comb_multiply_base_point; +use bouncycastle_ec::bp512r1_scalar::Bp512r1Scalar; +use bouncycastle_ecdsa::keys_bp512r1::{ECDSABp512r1PrivateKey, ECDSABp512r1PublicKey}; +use bouncycastle_ecdsa::rfc6979_bp512r1::generate_k; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_sha2::SHA512; + +struct Vector { + d: &'static str, + msg: &'static [u8], + k: &'static str, + r: &'static str, + s: &'static str, + ux: &'static str, + uy: &'static str, +} + +// One h.3 rejection: the accepted k is the second candidate, after one re-key of the HMAC chain. +const CASE_0: Vector = Vector { + d: "330EC367DE3D130E41B64A8D9CA805E18BE822EDD22F87F9914BFAF280DD7C1531A01101800F36D7D7F978F0A674894EAF61ACFE376CEAE9AE8692ADE621E465", + msg: b"", + k: "889C0E5302360A2DA00BD96ADFD79B0E13BB5EA37E17DF7B9BA21662B6D4884D0644FB00E1773BD044CFA9B2A17A0DFCA1718FC071424E198135336C32EAE0EE", + r: "AA8B911F06FB47FB8CEB47E9E4461DE62F252B786BFA4C0CE25FFE541E77C5A2F51661A49222440CBEE270E3C3DCD63F659A851A63F0FD956D99853764683068", + s: "75E16529258661D091F11F214D6A9E89C1857882F3F7BCD8D5DFE606D2FF29F11049683EF23C1034BFB580FBA85044DB627FA9D6AC093B37C8E7D7079C90C740", + ux: "4BB89D573182AB456CEE615C3A435A9AF10B7B9BBD35269EA3177993DB6F07A7810D34A808018660A4EE88FB14D220F986BDE200B37841FF8FC8DA3E87A99286", + uy: "3B7A607F3E359062492F5D078FEBD595B14A898C4117CFF633CAFB6A57EC5F38390BE97E618B482F10C657D89CC47A16FA93F73A8C99FE050907DDC758DA1BA1", +}; + +// One h.3 rejection: the accepted k is the second candidate, after one re-key of the HMAC chain. +const CASE_1: Vector = Vector { + d: "17BAA6642A507BD5336ED46492D516FEE5A381A0C9D32A03D4CCAF167412C30CA67E90EC30BC65D57594E68E791FA9ADF8E3DD706ECB52454C5082AF09E88A09", + msg: b"sample", + k: "9890B70A7901E0F0E6A15617B6B38782C90DB0F15ACB214863B87DB4CA5DE4153C3261A69E50482FB77D8F30121EDD3E7AC6E3F3D44C85F0623BC47ED7C17711", + r: "A3452C5439209AEC837D1558214D3BB3CC8837D4B6CDDF472C66704759879071A7DA9E730E429E7E41A1733A339BAF6772DB7DEB540F29F05275CDAEBE87773D", + s: "786A229192486D2C69E22F7591C0AF7D5868C3884CA8C4FBD28CB970282B6212B5186A662104523F332C38D0F344ABEB261D63531DC7B5357A446E9DADB5F79A", + ux: "7BD4906ECD341E0EBB21359D14B57905BB781DB73C966B95E869FC064E8187E9227C659D4915763E9A3440C9B1D23A2AA8739EE02310CED5173E043F8836D509", + uy: "701F14AB67CA27522018F09B9754B90C8DE9BDCE2E84AE5B8AB7416F5AE62087AFB54580EDC9EBE6BF59995B725A93147FFB4923CDB6357BE0FFC94EF603076B", +}; + +const CASE_2: Vector = Vector { + d: "04D755462F24359A7D8977EF083C9B7AB8BB8C370511F14D57DD04F7D0CF27D0706A18E7C67212978E9A42F0FDF3F46FFCA3DF48D3489CCF392312B11A4F6AF8", + msg: b"test message for RFC 6979 brainpoolP512r1 cross-check", + k: "55C8EE013FAA793FC4B0C180F1721194F8DEB43EFF8253AA6B6980139CD5D066BC9B7C33FF7E1D97384FC250A8791649CD576F895C76DDC51D58518EE5691D70", + r: "2FDB9F8380B2EA96EAE9889F37022143EC557DAE7F2CAC0FA95DDC174CD0FEEBDB6675184CC75EC74589BA16239841B6E403996D8B668A6C0CEFEC8891C27EFE", + s: "8E05C6616A5F9423E34639DE67A5EDDAA4A073BF614774A1119290059BDA0A37D186A8BAFFC731C51E81362E7AE8AB40187620B6FD841319DE569105C5F73966", + ux: "7C9B80400B43B7BFBE42EFF7F2F1E681D0667EDAC9FE2835C4B67BC8802553ADC37E415B05C2181B26D0526749EA166FB5817FB30D6BADD6F6DDE2D558CCB403", + uy: "8D262DB01393F689FB35A205022C4FF69C14E063B6361E22B11ABA0352082386890DA3FAD1B0759F9ADD68242C3BD52D371ABB4469B48F6EF0B2C4F3F0CD7210", +}; + +const CASE_3: Vector = Vector { + d: "1E484B7A312A44DD6494CF57049E427467DFEBC315C77C8AF429FCF59430B281AEAE2975109B1AD4509AF5ED26B1C7131827FB5CB822F0D43CE7AD71DB8870C7", + msg: &[0u8; 32], + k: "A9D15730186A662FFD3EE3BAE9F3EBB4DE47E5F42C9AFFFE89F2BF986259608190A602FD1FA80856E9F9F00C6FEB4D5A5D3B6E291F0096D0E3C653D08146F565", + r: "7FC4472D23D322A8D47D84B3C74DC0EA57228D86EA5BCEB69AFAA3B004D939AE9F3E2E5126B68BC942C50CB7D33D0F5102CE3A21DCE609D35287E95675CCB0A9", + s: "564716167DA75BD78AB145EAD26916DA59E32F5F67A56002BDCF038462DAD3A648BA92DB8C6E9552C43C2BF8078D606C37836269BA7B320D26D314B9922F136D", + ux: "8090AECA3CA3421547B3EF061A6E95D56A9608288581647FA0FF2AC3C2DE41D6EFED16185A37140B8EFE0446ECD2B9D4A779A28AA1BB830AD0C81B4916C44B39", + uy: "6EB7ED76D688B7C89179D29D2653B674DEBD1A15FF210F50D6874724DE654D8B70786AB4B1C6FEC36240464497AD843E1A091FEFED76F5740BE58CB2999194E4", +}; + +const CASE_4: Vector = Vector { + d: "4B9E378BEEAC6812FF4C1BBBA67BEF083AC2CE16303249B912258C63556A44E17CEE4B4D567578F26A89C3FC0098EFE577CBF8FCAC22AAE164F74D7169CEB006", + msg: &[0xffu8; 10], + k: "8BA549E6BE1B467B08535F30F5D5593171892DF18E17E6C3E48C5E083DE9E90CCAB1DD59B75983E74D32EC4E5ACA238D1790737DE5C47CC7B239FBD8829CFE71", + r: "0E91E240E97F6D7B9CB5F5CC99A16FA33D71B1C2AD63BD2B559D223B0E90BFB513BC5EEF94B0435997DD329C286F9C74EC8665C45AC88B1B1616FC5F2C73DA87", + s: "79E80AB611E3BEADF15FD136FBA8D4D5E7DFE2D47DACA1E34D8238833E9CFC9B112443147A662F79368D02F230E77B9EC0F40B1B12BE5F4367E07EC5E2953383", + ux: "20F1796F4B50A146E6D04C75A3B0D899E6FEACE362D247FCAE6CB2A235F860D7605EDB78E3A56DB9401DBB0DA8A82D491E2F945DDBB16BC3384D4A2154AFD62A", + uy: "073A8842B4732D73B6F7213F2DAAC046316A3CDA5D9C4B81B894FEF063340379103826C1CB111DA3161B630FCB6952B49E709903286B6914C1043F75450C1EEB", +}; + +// Pins the h.3 continue-chain deliberately: five candidates in a row are >= n, so the accepted k +// follows five `K = HMAC_K(V || 0x00)` re-keys -- the (d, msg) pair was searched for exactly that. +const CASE_5: Vector = Vector { + d: "2ACDAFAA028C371E193290ECD1F609D3D83EE2CEA56DFBB70023B9242AF6DF75D55ABB2A9EC6C836EE206890D5425531336AC8A86FB7A41E074AA01D2C395E5C", + msg: b"brainpoolP512r1 rejection test 113", + k: "4ECFD8C45E6ED73F9B59A4F4BE2ACB4398D1CD73FAFD46CD5AF37A7E30B711195F9D6B12D6E8237CDC5F6B67AB7CC36A381335F3811935E1F76BA1E02363EB12", + r: "006EAB155B1A7883362C9FE0A0E84A47A8F4086D594F5F12A3BAC0A0628255DC6860A0649095F365C5B0F571144D00F044188B0DD2471E41BDF100A6FFA85762", + s: "92A29C2A899A8F513957A66A0D42F4F7B8E330ECF44A0272AF82568A3EB981169EBB81716A6AE8B395699E64F7C63FAA06C80514717E6F72E8D31536796D0117", + ux: "048760C2E0AC7AE2D36049ACB2406A37C6CC4160871B6E0BD69C0F92D95DD626CCFD90717F180EFF435820E8817756AB03D741480C80322C370FA9B8C3C2DBD2", + uy: "686B835D888512359FD2EBF73E233E0CFD5C82EDCE1FC9E891BD253C981D55A36D8FEAB7FC1EAAE1DE6E9D4ADD1A6A79A38FC553EA92A75A326ED6FAE1BEF499", +}; + +const CASES: [&Vector; 6] = [&CASE_0, &CASE_1, &CASE_2, &CASE_3, &CASE_4, &CASE_5]; + +fn bytes64(hex: &str) -> [u8; 64] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +fn public_key_for(v: &Vector) -> ECDSABp512r1PublicKey { + let mut uncompressed = [0u8; 129]; + uncompressed[0] = 0x04; + uncompressed[1..65].copy_from_slice(&bytes64(v.ux)); + uncompressed[65..129].copy_from_slice(&bytes64(v.uy)); + ECDSABp512r1PublicKey::from_bytes(&uncompressed).unwrap() +} + +#[test] +fn public_key_matches_vectors() { + for v in CASES { + let d = Bp512r1Scalar::from_be_bytes(&bytes64(v.d)); + let q = comb_multiply_base_point(&d); + let (x, y) = q.to_affine().unwrap(); + assert_eq!( + x.to_limbs(), + bouncycastle_ec::bp512r1_sec1::limbs_from_be_bytes(&bytes64(v.ux)), + "Qx for d = {}", + v.d + ); + assert_eq!( + y.to_limbs(), + bouncycastle_ec::bp512r1_sec1::limbs_from_be_bytes(&bytes64(v.uy)), + "Qy for d = {}", + v.d + ); + } +} + +#[test] +fn generate_k_matches_vectors() { + for v in CASES { + let d = Bp512r1Scalar::from_be_bytes(&bytes64(v.d)); + let h: [u8; 64] = SHA512::hash(v.msg)[..64].try_into().unwrap(); + let k = generate_k(&d, &h); + assert_eq!(k, Bp512r1Scalar::from_be_bytes(&bytes64(v.k)), "k for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_sign_matches_vectors() { + for v in CASES { + let sk = ECDSABp512r1PrivateKey::from_bytes(&bytes64(v.d)).unwrap(); + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + let mut expected = [0u8; 128]; + expected[..64].copy_from_slice(&bytes64(v.r)); + expected[64..].copy_from_slice(&bytes64(v.s)); + assert_eq!(sig, expected, "signature for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_signature_verifies() { + for v in CASES { + let sk = ECDSABp512r1PrivateKey::from_bytes(&bytes64(v.d)).unwrap(); + let pk = public_key_for(v); + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + pk.verify(v.msg, None, &sig).unwrap(); + } +} diff --git a/crypto/ecdsa/tests/rfc6979_p256_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_p256_vectors_tests.rs new file mode 100644 index 000000000..f81af886e --- /dev/null +++ b/crypto/ecdsa/tests/rfc6979_p256_vectors_tests.rs @@ -0,0 +1,92 @@ +//! RFC 6979 Appendix A.2.5 ("ECDSA, 256 Bits (Prime Field)") known-answer tests: the key pair, the +//! deterministic per-message secret `k`, and the resulting signature `(r, s)`, for `curve: NIST +//! P-256` with SHA-256, messages `"sample"` and `"test"`. + +use bouncycastle_core::signature_key::{DeterministicSigningKey, VerifyingKey}; +use bouncycastle_core::traits::HashOneShot; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::p256_comb::comb_multiply_base_point; +use bouncycastle_ec::p256_scalar::P256Scalar; +use bouncycastle_ecdsa::keys_p256::{ECDSAP256PrivateKey, ECDSAP256PublicKey}; +use bouncycastle_ecdsa::rfc6979_p256::generate_k; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_sha2::SHA256; + +const X: &str = "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721"; +const UX: &str = "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6"; +const UY: &str = "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299"; + +struct Vector { + msg: &'static [u8], + k: &'static str, + r: &'static str, + s: &'static str, +} + +const SAMPLE: Vector = Vector { + msg: b"sample", + k: "A6E3C57DD01ABE90086538398355DD4C3B17AA873382B0F24D6129493D8AAD60", + r: "EFD48B2AACB6A8FD1140DD9CD45E81D69D2C877B56AAF991C34D0EA84EAF3716", + s: "F7CB1C942D657C41D436C7A1B6E29F65F3E900DBB9AFF4064DC4AB2F843ACDA8", +}; + +const TEST: Vector = Vector { + msg: b"test", + k: "D16B6AE827F17175E040871A1C7EC3500192C4C92677336EC2537ACAEE0008E0", + r: "F1ABB023518351CD71D881567B1EA663ED3EFCF6C5132B354F28D3B0B7D38367", + s: "019F4113742A2B14BD25926B49C649155F267E60D3814B4C0CC84250E46F0083", +}; + +fn bytes32(hex: &str) -> [u8; 32] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +fn private_key() -> ECDSAP256PrivateKey { + ECDSAP256PrivateKey::from_bytes(&bytes32(X)).unwrap() +} + +#[test] +fn public_key_matches_vector() { + let d = P256Scalar::from_be_bytes(&bytes32(X)); + let q = comb_multiply_base_point(&d); + let (x, y) = q.to_affine().unwrap(); + assert_eq!(x.to_limbs(), bouncycastle_ec::p256_sec1::limbs_from_be_bytes(&bytes32(UX))); + assert_eq!(y.to_limbs(), bouncycastle_ec::p256_sec1::limbs_from_be_bytes(&bytes32(UY))); +} + +#[test] +fn generate_k_matches_vectors() { + let d = P256Scalar::from_be_bytes(&bytes32(X)); + for v in [&SAMPLE, &TEST] { + let h: [u8; 32] = SHA256::hash(v.msg)[..32].try_into().unwrap(); + let k = generate_k(&d, &h); + assert_eq!(k, P256Scalar::from_be_bytes(&bytes32(v.k)), "k for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_sign_matches_vectors() { + let sk = private_key(); + for v in [&SAMPLE, &TEST] { + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + let mut expected = [0u8; 64]; + expected[..32].copy_from_slice(&bytes32(v.r)); + expected[32..].copy_from_slice(&bytes32(v.s)); + assert_eq!(sig, expected, "signature for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_signature_verifies() { + let sk = private_key(); + let mut uncompressed = [0u8; 65]; + uncompressed[0] = 0x04; + uncompressed[1..33].copy_from_slice(&bytes32(UX)); + uncompressed[33..65].copy_from_slice(&bytes32(UY)); + let pk = ECDSAP256PublicKey::from_bytes(&uncompressed).unwrap(); + + for v in [&SAMPLE, &TEST] { + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + pk.verify(v.msg, None, &sig).unwrap(); + } +} diff --git a/crypto/ecdsa/tests/rfc6979_p256k1_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_p256k1_vectors_tests.rs new file mode 100644 index 000000000..05bdfe2bc --- /dev/null +++ b/crypto/ecdsa/tests/rfc6979_p256k1_vectors_tests.rs @@ -0,0 +1,144 @@ +//! Cross-check against an independent Python implementation of RFC 6979 §3.2 (using Python's own +//! `hmac`/`hashlib`, not derived from this crate) for secp256k1/SHA-256: RFC 6979 has no official +//! Appendix A.2.x vectors for this curve (its appendix only covers the NIST curves P-192 through +//! P-521), so this is the closest equivalent -- five independently-generated `(d, msg)` pairs +//! (empty, short ASCII, longer ASCII, all-zero, all-`0xff`), each with the deterministic `k`, the +//! resulting `(r, s)`, and `Q = [d]G` computed via the standard affine group law, `random.seed +//! 1234567`. + +use bouncycastle_core::signature_key::{DeterministicSigningKey, VerifyingKey}; +use bouncycastle_core::traits::HashOneShot; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::p256k1_comb::comb_multiply_base_point; +use bouncycastle_ec::p256k1_scalar::P256K1Scalar; +use bouncycastle_ecdsa::keys_p256k1::{ECDSASecp256K1PrivateKey, ECDSASecp256K1PublicKey}; +use bouncycastle_ecdsa::rfc6979_p256k1::generate_k; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_sha2::SHA256; + +struct Vector { + d: &'static str, + msg: &'static [u8], + k: &'static str, + r: &'static str, + s: &'static str, + ux: &'static str, + uy: &'static str, +} + +const CASE_0: Vector = Vector { + d: "1201AF823A0A4FE05170CFF294DC13FC115AC6CB3C58FD846EC5C07FF6908E54", + msg: b"", + k: "CD78A8941915185A2FE30B89C7C20F0D48D1BE5FCF610FD1BE5685E98ED94D08", + r: "E9492B86659F129207FD328BE2A8306B7AE55191F281000F233C5D65E538F1BB", + s: "185A99E918626EC188FE2F2A46F47668DEAE837557BDE9BDFE65216BDC708ABA", + ux: "1F2193519AB30887B47ADC572A873070806D5D074D8FFAA4CC0972E97F7EE7E9", + uy: "D637FBA4435057B89F571C622121B125380E7433215130120D8208478A99C547", +}; + +const CASE_1: Vector = Vector { + d: "E84644DE88C3D52B0C43F62912F8A9A50A3ECED49C8BE3E693AF8A68F77282BE", + msg: b"sample", + k: "28C982F2D681B65DC8CA126205D5864A6ADEC5CC3FCF2B30BF89DCC83AD348F6", + r: "2E55A9673BCFB47B7F224B3D6C7643113E11954AC6542885B14501DD99ACB72C", + s: "FB86608F25CA76A8A9961E8DB1A925505A768214512EEC9DB130AE90CAB11017", + ux: "2BE0FFB78E3BCD795B5B08679C2BC7128B8D8F08803B24B86BBB02209C73ECBD", + uy: "52D6692A25601A426D88AE8DD507D44763AC213A8A9957A1C4C7646DA8C24E64", +}; + +const CASE_2: Vector = Vector { + d: "DE05EF1E2CD6AD8E3FF33516D38E5432EB355B52C8FA65AB0E3605CD9AB15D0C", + msg: b"test message for RFC 6979 secp256k1 cross-check", + k: "ECB953D84E41FF5B14980018BF1776BAA052A9249F2A4662294EA1CB2E82EC25", + r: "405B6868BF58AEFD479747434DFF86B0DF24A71E4EF27179E104757654017D18", + s: "2EA4999F9BE624D880317A9A06E7E96A4DDAB8CEAE0AF3188249734B81D49520", + ux: "63A46AA9E76D052ECD957F41253F5CA04E5CC509D6F15426174DB239B61110B8", + uy: "64FC9614DE8D40FFD02A73F3DA1DBBA240FF49A835D1A84A3BB780EFE29A7A37", +}; + +const CASE_3: Vector = Vector { + d: "AC2C81558B8DF6D7A58A40E3AEC4FAA16DFD4602CC3F525D91DA942BF2F44204", + msg: &[0u8; 32], + k: "AD82D09CD81EA99ACCBC33E55DD46F0401F2C2FF0A984C33FF6EA2A048FFB103", + r: "DEE53E887C06D65D6F47C86D7FA947033666FBC57B32F0BE0E47E580DE1BADAB", + s: "6984C174EFF22BAAC48FE6BCAB4A95927E720894E31304970B2FDB82E3D3CA7E", + ux: "4E4CEC72F06A61EB1024B996901A31F66BF3D54BB4046AD007AA19747253D58A", + uy: "7121A6621BFA0C238EB02CFCB7DBCB985A42317C8D5417F03CA8BC72B5148740", +}; + +const CASE_4: Vector = Vector { + d: "7E7B91BCA9655DC7AA94AFFD753150AEAF395DDF588590D1CB31A5A541346EC1", + msg: &[0xffu8; 10], + k: "429AB5983FEFEF2BA0C58EB6AC48FD5C5E9BB9E283665D360959E377ABCFF2EE", + r: "9F6DC42E943A9815B5AFF764A79EA85EC5C0584DE5C86E96CEB1C4089146E945", + s: "D536E3D82BF405282FB688CA3DF3CDD5DD520075735E9A19E8878E0CF4277F70", + ux: "56587EF8DED5FD2BDE4F00C559C52CB307AD8171568F2028803195E667622833", + uy: "A8513344175B03B64CDD5A72BE2B9A3434215C740FAB01F6906473CB7806BC4C", +}; + +const CASES: [&Vector; 5] = [&CASE_0, &CASE_1, &CASE_2, &CASE_3, &CASE_4]; + +fn bytes32(hex: &str) -> [u8; 32] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +fn public_key_for(v: &Vector) -> ECDSASecp256K1PublicKey { + let mut uncompressed = [0u8; 65]; + uncompressed[0] = 0x04; + uncompressed[1..33].copy_from_slice(&bytes32(v.ux)); + uncompressed[33..65].copy_from_slice(&bytes32(v.uy)); + ECDSASecp256K1PublicKey::from_bytes(&uncompressed).unwrap() +} + +#[test] +fn public_key_matches_vectors() { + for v in CASES { + let d = P256K1Scalar::from_be_bytes(&bytes32(v.d)); + let q = comb_multiply_base_point(&d); + let (x, y) = q.to_affine().unwrap(); + assert_eq!( + x.to_limbs(), + bouncycastle_ec::p256k1_sec1::limbs_from_be_bytes(&bytes32(v.ux)), + "Qx for d = {}", + v.d + ); + assert_eq!( + y.to_limbs(), + bouncycastle_ec::p256k1_sec1::limbs_from_be_bytes(&bytes32(v.uy)), + "Qy for d = {}", + v.d + ); + } +} + +#[test] +fn generate_k_matches_vectors() { + for v in CASES { + let d = P256K1Scalar::from_be_bytes(&bytes32(v.d)); + let h: [u8; 32] = SHA256::hash(v.msg)[..32].try_into().unwrap(); + let k = generate_k(&d, &h); + assert_eq!(k, P256K1Scalar::from_be_bytes(&bytes32(v.k)), "k for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_sign_matches_vectors() { + for v in CASES { + let sk = ECDSASecp256K1PrivateKey::from_bytes(&bytes32(v.d)).unwrap(); + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + let mut expected = [0u8; 64]; + expected[..32].copy_from_slice(&bytes32(v.r)); + expected[32..].copy_from_slice(&bytes32(v.s)); + assert_eq!(sig, expected, "signature for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_signature_verifies() { + for v in CASES { + let sk = ECDSASecp256K1PrivateKey::from_bytes(&bytes32(v.d)).unwrap(); + let pk = public_key_for(v); + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + pk.verify(v.msg, None, &sig).unwrap(); + } +} diff --git a/crypto/ecdsa/tests/rfc6979_p384_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_p384_vectors_tests.rs new file mode 100644 index 000000000..944e182c0 --- /dev/null +++ b/crypto/ecdsa/tests/rfc6979_p384_vectors_tests.rs @@ -0,0 +1,92 @@ +//! RFC 6979 Appendix A.2.6 ("ECDSA, 384 Bits (Prime Field)") known-answer tests: the key pair, the +//! deterministic per-message secret `k`, and the resulting signature `(r, s)`, for `curve: NIST +//! P-384` with SHA-384, messages `"sample"` and `"test"`. + +use bouncycastle_core::signature_key::{DeterministicSigningKey, VerifyingKey}; +use bouncycastle_core::traits::HashOneShot; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::p384_comb::comb_multiply_base_point; +use bouncycastle_ec::p384_scalar::P384Scalar; +use bouncycastle_ecdsa::keys_p384::{ECDSAP384PrivateKey, ECDSAP384PublicKey}; +use bouncycastle_ecdsa::rfc6979_p384::generate_k; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_sha2::SHA384; + +const X: &str = "6B9D3DAD2E1B8C1C05B19875B6659F4DE23C3B667BF297BA9AA47740787137D896D5724E4C70A825F872C9EA60D2EDF5"; +const UX: &str = "EC3A4E415B4E19A4568618029F427FA5DA9A8BC4AE92E02E06AAE5286B300C64DEF8F0EA9055866064A254515480BC13"; +const UY: &str = "8015D9B72D7D57244EA8EF9AC0C621896708A59367F9DFB9F54CA84B3F1C9DB1288B231C3AE0D4FE7344FD2533264720"; + +struct Vector { + msg: &'static [u8], + k: &'static str, + r: &'static str, + s: &'static str, +} + +const SAMPLE: Vector = Vector { + msg: b"sample", + k: "94ED910D1A099DAD3254E9242AE85ABDE4BA15168EAF0CA87A555FD56D10FBCA2907E3E83BA95368623B8C4686915CF9", + r: "94EDBB92A5ECB8AAD4736E56C691916B3F88140666CE9FA73D64C4EA95AD133C81A648152E44ACF96E36DD1E80FABE46", + s: "99EF4AEB15F178CEA1FE40DB2603138F130E740A19624526203B6351D0A3A94FA329C145786E679E7B82C71A38628AC8", +}; + +const TEST: Vector = Vector { + msg: b"test", + k: "015EE46A5BF88773ED9123A5AB0807962D193719503C527B031B4C2D225092ADA71F4A459BC0DA98ADB95837DB8312EA", + r: "8203B63D3C853E8D77227FB377BCF7B7B772E97892A80F36AB775D509D7A5FEB0542A7F0812998DA8F1DD3CA3CF023DB", + s: "DDD0760448D42D8A43AF45AF836FCE4DE8BE06B485E9B61B827C2F13173923E06A739F040649A667BF3B828246BAA5A5", +}; + +fn bytes48(hex: &str) -> [u8; 48] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +fn private_key() -> ECDSAP384PrivateKey { + ECDSAP384PrivateKey::from_bytes(&bytes48(X)).unwrap() +} + +#[test] +fn public_key_matches_vector() { + let d = P384Scalar::from_be_bytes(&bytes48(X)); + let q = comb_multiply_base_point(&d); + let (x, y) = q.to_affine().unwrap(); + assert_eq!(x.to_limbs(), bouncycastle_ec::p384_sec1::limbs_from_be_bytes(&bytes48(UX))); + assert_eq!(y.to_limbs(), bouncycastle_ec::p384_sec1::limbs_from_be_bytes(&bytes48(UY))); +} + +#[test] +fn generate_k_matches_vectors() { + let d = P384Scalar::from_be_bytes(&bytes48(X)); + for v in [&SAMPLE, &TEST] { + let h: [u8; 48] = SHA384::hash(v.msg)[..48].try_into().unwrap(); + let k = generate_k(&d, &h); + assert_eq!(k, P384Scalar::from_be_bytes(&bytes48(v.k)), "k for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_sign_matches_vectors() { + let sk = private_key(); + for v in [&SAMPLE, &TEST] { + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + let mut expected = [0u8; 96]; + expected[..48].copy_from_slice(&bytes48(v.r)); + expected[48..].copy_from_slice(&bytes48(v.s)); + assert_eq!(sig, expected, "signature for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_signature_verifies() { + let sk = private_key(); + let mut uncompressed = [0u8; 97]; + uncompressed[0] = 0x04; + uncompressed[1..49].copy_from_slice(&bytes48(UX)); + uncompressed[49..97].copy_from_slice(&bytes48(UY)); + let pk = ECDSAP384PublicKey::from_bytes(&uncompressed).unwrap(); + + for v in [&SAMPLE, &TEST] { + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + pk.verify(v.msg, None, &sig).unwrap(); + } +} diff --git a/crypto/ecdsa/tests/rfc6979_p521_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_p521_vectors_tests.rs new file mode 100644 index 000000000..97b60bd58 --- /dev/null +++ b/crypto/ecdsa/tests/rfc6979_p521_vectors_tests.rs @@ -0,0 +1,95 @@ +//! RFC 6979 Appendix A.2.7 ("ECDSA, 521 Bits (Prime Field)") known-answer tests: the key pair, the +//! deterministic per-message secret `k`, and the resulting signature `(r, s)`, for `curve: NIST +//! P-521` with SHA-512, messages `"sample"` and `"test"`. Values extracted programmatically from +//! a fresh copy of the RFC text (not hand-transcribed) to avoid the line-wrap transcription error +//! this crate's P-384 vectors hit; each is 131 hex digits in the RFC (`ceil(521/4)`), zero-padded +//! here with a leading `0` to the fixed 132-digit (66-byte) SEC 1 width. + +use bouncycastle_core::signature_key::{DeterministicSigningKey, VerifyingKey}; +use bouncycastle_core::traits::HashOneShot; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::p521_comb::comb_multiply_base_point; +use bouncycastle_ec::p521_scalar::P521Scalar; +use bouncycastle_ecdsa::keys_p521::{ECDSAP521PrivateKey, ECDSAP521PublicKey}; +use bouncycastle_ecdsa::rfc6979_p521::generate_k; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_sha2::SHA512; + +const X: &str = "00FAD06DAA62BA3B25D2FB40133DA757205DE67F5BB0018FEE8C86E1B68C7E75CAA896EB32F1F47C70855836A6D16FCC1466F6D8FBEC67DB89EC0C08B0E996B83538"; +const UX: &str = "01894550D0785932E00EAA23B694F213F8C3121F86DC97A04E5A7167DB4E5BCD371123D46E45DB6B5D5370A7F20FB633155D38FFA16D2BD761DCAC474B9A2F5023A4"; +const UY: &str = "00493101C962CD4D2FDDF782285E64584139C2F91B47F87FF82354D6630F746A28A0DB25741B5B34A828008B22ACC23F924FAAFBD4D33F81EA66956DFEAA2BFDFCF5"; + +struct Vector { + msg: &'static [u8], + k: &'static str, + r: &'static str, + s: &'static str, +} + +const SAMPLE: Vector = Vector { + msg: b"sample", + k: "01DAE2EA071F8110DC26882D4D5EAE0621A3256FC8847FB9022E2B7D28E6F10198B1574FDD03A9053C08A1854A168AA5A57470EC97DD5CE090124EF52A2F7ECBFFD3", + r: "00C328FAFCBD79DD77850370C46325D987CB525569FB63C5D3BC53950E6D4C5F174E25A1EE9017B5D450606ADD152B534931D7D4E8455CC91F9B15BF05EC36E377FA", + s: "00617CCE7CF5064806C467F678D3B4080D6F1CC50AF26CA209417308281B68AF282623EAA63E5B5C0723D8B8C37FF0777B1A20F8CCB1DCCC43997F1EE0E44DA4A67A", +}; + +const TEST: Vector = Vector { + msg: b"test", + k: "016200813020EC986863BEDFC1B121F605C1215645018AEA1A7B215A564DE9EB1B38A67AA1128B80CE391C4FB71187654AAA3431027BFC7F395766CA988C964DC56D", + r: "013E99020ABF5CEE7525D16B69B229652AB6BDF2AFFCAEF38773B4B7D08725F10CDB93482FDCC54EDCEE91ECA4166B2A7C6265EF0CE2BD7051B7CEF945BABD47EE6D", + s: "01FBD0013C674AA79CB39849527916CE301C66EA7CE8B80682786AD60F98F7E78A19CA69EFF5C57400E3B3A0AD66CE0978214D13BAF4E9AC60752F7B155E2DE4DCE3", +}; + +fn bytes66(hex: &str) -> [u8; 66] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +fn private_key() -> ECDSAP521PrivateKey { + ECDSAP521PrivateKey::from_bytes(&bytes66(X)).unwrap() +} + +#[test] +fn public_key_matches_vector() { + let d = P521Scalar::from_be_bytes(&bytes66(X)); + let q = comb_multiply_base_point(&d); + let (x, y) = q.to_affine().unwrap(); + assert_eq!(x.to_limbs(), bouncycastle_ec::p521_sec1::limbs_from_be_bytes(&bytes66(UX))); + assert_eq!(y.to_limbs(), bouncycastle_ec::p521_sec1::limbs_from_be_bytes(&bytes66(UY))); +} + +#[test] +fn generate_k_matches_vectors() { + let d = P521Scalar::from_be_bytes(&bytes66(X)); + for v in [&SAMPLE, &TEST] { + let h: [u8; 64] = SHA512::hash(v.msg)[..64].try_into().unwrap(); + let k = generate_k(&d, &h); + assert_eq!(k, P521Scalar::from_be_bytes(&bytes66(v.k)), "k for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_sign_matches_vectors() { + let sk = private_key(); + for v in [&SAMPLE, &TEST] { + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + let mut expected = [0u8; 132]; + expected[..66].copy_from_slice(&bytes66(v.r)); + expected[66..].copy_from_slice(&bytes66(v.s)); + assert_eq!(sig, expected, "signature for message {:?}", v.msg); + } +} + +#[test] +fn deterministic_signature_verifies() { + let sk = private_key(); + let mut uncompressed = [0u8; 133]; + uncompressed[0] = 0x04; + uncompressed[1..67].copy_from_slice(&bytes66(UX)); + uncompressed[67..133].copy_from_slice(&bytes66(UY)); + let pk = ECDSAP521PublicKey::from_bytes(&uncompressed).unwrap(); + + for v in [&SAMPLE, &TEST] { + let sig = sk.sign_deterministic(v.msg, None).unwrap(); + pk.verify(v.msg, None, &sig).unwrap(); + } +} diff --git a/crypto/ecdsa/tests/wycheproof_bp256r1.rs b/crypto/ecdsa/tests/wycheproof_bp256r1.rs new file mode 100644 index 000000000..5a4800393 --- /dev/null +++ b/crypto/ecdsa/tests/wycheproof_bp256r1.rs @@ -0,0 +1,147 @@ +//! Test against the project wycheproof repo: `ecdsa_brainpoolP256r1_sha256_p1363_test.json` (raw +//! `r || s`, against [`ECDSABp256r1::verify`]) and `ecdsa_brainpoolP256r1_sha256_test.json` (DER +//! `SEQUENCE { r, s }`, RFC 3279 §2.2.3, against [`ECDSABp256r1::verify_der`]). See +//! `wycheproof.rs`'s docs for the sibling-clone convention and what wycheproof's value is here +//! (malleability/invalid-encoding coverage, not signature-generation KATs). + +use bouncycastle_core::signature_key::VerifyingKey; +use bouncycastle_core::traits::SignaturePublicKey; +use bouncycastle_ecdsa::keys_bp256r1::ECDSABp256r1PublicKey; +use bouncycastle_hex::decode as hex_decode; +use serde_json::Value; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +/// Panics, failing the test, if the wycheproof clone is missing: these suites are the only +/// adversarial check on this crate's verification, and a green run that never read them would be +/// indistinguishable from one that did. (For `cargo mutants`, symlink the clone at +/// `/tmp/wycheproof`; its copied tree resolves the relative path there.) +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); these suites require it rather than skipping" + ); +} + +#[test] +fn ecdsa_brainpool_p256r1_sha256_p1363_test() { + let contents = get_test_data("ecdsa_brainpoolP256r1_sha256_p1363_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-256"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = ECDSABp256r1PublicKey::from_bytes(&pk_bytes).ok(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_brainpoolP256r1_sha256_p1363_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} + +/// The DER counterpart of the above: `ecdsa_brainpoolP256r1_sha256_test.json`, against +/// [`ECDSABp256r1::verify_der`]. +#[test] +fn ecdsa_brainpool_p256r1_sha256_der_test() { + let contents = get_test_data("ecdsa_brainpoolP256r1_sha256_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-256"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = ECDSABp256r1PublicKey::from_bytes(&pk_bytes).ok(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify_der(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_brainpoolP256r1_sha256_der_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} diff --git a/crypto/ecdsa/tests/wycheproof_bp384r1.rs b/crypto/ecdsa/tests/wycheproof_bp384r1.rs new file mode 100644 index 000000000..f870767f2 --- /dev/null +++ b/crypto/ecdsa/tests/wycheproof_bp384r1.rs @@ -0,0 +1,147 @@ +//! Test against the project wycheproof repo: `ecdsa_brainpoolP384r1_sha384_p1363_test.json` (raw +//! `r || s`, against [`ECDSABp384r1::verify`]) and `ecdsa_brainpoolP384r1_sha384_test.json` (DER +//! `SEQUENCE { r, s }`, RFC 3279 §2.2.3, against [`ECDSABp384r1::verify_der`]). See +//! `wycheproof.rs`'s docs for the sibling-clone convention and what wycheproof's value is here +//! (malleability/invalid-encoding coverage, not signature-generation KATs). + +use bouncycastle_core::signature_key::VerifyingKey; +use bouncycastle_core::traits::SignaturePublicKey; +use bouncycastle_ecdsa::keys_bp384r1::ECDSABp384r1PublicKey; +use bouncycastle_hex::decode as hex_decode; +use serde_json::Value; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +/// Panics, failing the test, if the wycheproof clone is missing: these suites are the only +/// adversarial check on this crate's verification, and a green run that never read them would be +/// indistinguishable from one that did. (For `cargo mutants`, symlink the clone at +/// `/tmp/wycheproof`; its copied tree resolves the relative path there.) +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); these suites require it rather than skipping" + ); +} + +#[test] +fn ecdsa_brainpool_p384r1_sha384_p1363_test() { + let contents = get_test_data("ecdsa_brainpoolP384r1_sha384_p1363_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-384"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = ECDSABp384r1PublicKey::from_bytes(&pk_bytes).ok(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_brainpoolP384r1_sha384_p1363_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} + +/// The DER counterpart of the above: `ecdsa_brainpoolP384r1_sha384_test.json`, against +/// [`ECDSABp384r1::verify_der`]. +#[test] +fn ecdsa_brainpool_p384r1_sha384_der_test() { + let contents = get_test_data("ecdsa_brainpoolP384r1_sha384_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-384"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = ECDSABp384r1PublicKey::from_bytes(&pk_bytes).ok(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify_der(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_brainpoolP384r1_sha384_der_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} diff --git a/crypto/ecdsa/tests/wycheproof_bp512r1.rs b/crypto/ecdsa/tests/wycheproof_bp512r1.rs new file mode 100644 index 000000000..e2434bc14 --- /dev/null +++ b/crypto/ecdsa/tests/wycheproof_bp512r1.rs @@ -0,0 +1,147 @@ +//! Test against the project wycheproof repo: `ecdsa_brainpoolP512r1_sha512_p1363_test.json` (raw +//! `r || s`, against [`ECDSABp512r1::verify`]) and `ecdsa_brainpoolP512r1_sha512_test.json` (DER +//! `SEQUENCE { r, s }`, RFC 3279 §2.2.3, against [`ECDSABp512r1::verify_der`]). See +//! `wycheproof.rs`'s docs for the sibling-clone convention and what wycheproof's value is here +//! (malleability/invalid-encoding coverage, not signature-generation KATs). + +use bouncycastle_core::signature_key::VerifyingKey; +use bouncycastle_core::traits::SignaturePublicKey; +use bouncycastle_ecdsa::keys_bp512r1::ECDSABp512r1PublicKey; +use bouncycastle_hex::decode as hex_decode; +use serde_json::Value; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +/// Panics, failing the test, if the wycheproof clone is missing: these suites are the only +/// adversarial check on this crate's verification, and a green run that never read them would be +/// indistinguishable from one that did. (For `cargo mutants`, symlink the clone at +/// `/tmp/wycheproof`; its copied tree resolves the relative path there.) +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); these suites require it rather than skipping" + ); +} + +#[test] +fn ecdsa_brainpool_p512r1_sha512_p1363_test() { + let contents = get_test_data("ecdsa_brainpoolP512r1_sha512_p1363_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-512"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = ECDSABp512r1PublicKey::from_bytes(&pk_bytes).ok(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_brainpoolP512r1_sha512_p1363_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} + +/// The DER counterpart of the above: `ecdsa_brainpoolP512r1_sha512_test.json`, against +/// [`ECDSABp512r1::verify_der`]. +#[test] +fn ecdsa_brainpool_p512r1_sha512_der_test() { + let contents = get_test_data("ecdsa_brainpoolP512r1_sha512_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-512"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = ECDSABp512r1PublicKey::from_bytes(&pk_bytes).ok(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify_der(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_brainpoolP512r1_sha512_der_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} diff --git a/crypto/ecdsa/tests/wycheproof_p256.rs b/crypto/ecdsa/tests/wycheproof_p256.rs new file mode 100644 index 000000000..36a8a61f4 --- /dev/null +++ b/crypto/ecdsa/tests/wycheproof_p256.rs @@ -0,0 +1,174 @@ +//! Test against the project wycheproof repo available at: +//! https://github.com/C2SP/wycheproof +//! Requires that the wycheproof repository is cloned and available for testing at "../wycheproof" +//! relative to the root of this git project (see `bouncycastle-mldsa`'s `tests/wycheproof.rs` for +//! the same convention). +//! +//! Exercises `ecdsa_secp256r1_sha256_p1363_test.json` -- the "P1363" (raw, fixed-width `r || s`) +//! signature encoding, which is [`ECDSAP256`]'s default `sign`/`verify` encoding -- and the plain +//! (non-p1363) `ecdsa_secp256r1_sha256_test.json`, whose signatures are DER `SEQUENCE { r, s }` +//! (RFC 3279 §2.2.3), against [`ECDSAP256::verify_der`]. +//! +//! These are verify-only test sets (`EcdsaP1363Verify`/`EcdsaVerify`): wycheproof's value here is +//! the malleability, invalid-encoding (including malformed DER itself for the second file -- extra +//! bytes, non-minimal lengths, wrong tags) and edge-case `r`/`s` coverage, not KATs for signature +//! generation (RFC 6979's own Appendix A.2.5 vectors, exercised in `rfc6979_vectors_tests.rs`, +//! cover that). +//! +//! Verdicts: `valid` must verify and `invalid` must not; a vector marked `acceptable` (none in the +//! v1 files today) fails the test outright rather than being treated as either, so that adding +//! such vectors forces a per-flag decision here. `ecdsa_secp256k1_sha256_bitcoin_test.json` is +//! deliberately not read by the secp256k1 suite: its `SignatureMalleabilityBitcoin` vectors +//! require rejecting `s > n/2`, a Bitcoin consensus rule rather than part of ECDSA. + +use bouncycastle_core::signature_key::VerifyingKey; +use bouncycastle_core::traits::SignaturePublicKey; +use bouncycastle_ecdsa::keys_p256::ECDSAP256PublicKey; +use bouncycastle_hex::decode as hex_decode; +use serde_json::Value; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +/// Panics, failing the test, if the wycheproof clone is missing: these suites are the only +/// adversarial check on this crate's verification, and a green run that never read them would be +/// indistinguishable from one that did. (For `cargo mutants`, symlink the clone at +/// `/tmp/wycheproof`; its copied tree resolves the relative path there.) +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); these suites require it rather than skipping" + ); +} + +#[test] +fn ecdsa_secp256r1_sha256_p1363_test() { + let contents = get_test_data("ecdsa_secp256r1_sha256_p1363_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-256"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + // A malformed group public key (EdgeCasePublicKey groups deliberately use invalid points) + // makes every "valid" test in that group impossible by construction -- SP 800-186 Appendix + // D.1.1.1 validation on decode is exactly what's meant to reject those, so a decode failure + // here counts every test in the group as correctly rejected rather than failing the test. + let pk = match ECDSAP256PublicKey::from_bytes(&pk_bytes) { + Ok(pk) => Some(pk), + Err(_) => None, + }; + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_secp256r1_sha256_p1363_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} + +/// The DER counterpart of the above: same suite, `ecdsa_secp256r1_sha256_test.json` (DER-encoded +/// signatures), checked against [`ECDSAP256::verify_der`] instead of `verify`. +#[test] +fn ecdsa_secp256r1_sha256_der_test() { + let contents = get_test_data("ecdsa_secp256r1_sha256_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-256"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = match ECDSAP256PublicKey::from_bytes(&pk_bytes) { + Ok(pk) => Some(pk), + Err(_) => None, + }; + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify_der(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_secp256r1_sha256_der_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} diff --git a/crypto/ecdsa/tests/wycheproof_p256k1.rs b/crypto/ecdsa/tests/wycheproof_p256k1.rs new file mode 100644 index 000000000..271be7729 --- /dev/null +++ b/crypto/ecdsa/tests/wycheproof_p256k1.rs @@ -0,0 +1,147 @@ +//! Test against the project wycheproof repo: `ecdsa_secp256k1_sha256_p1363_test.json` (raw `r || +//! s`, against [`ECDSASecp256K1::verify`]) and `ecdsa_secp256k1_sha256_test.json` (DER +//! `SEQUENCE { r, s }`, RFC 3279 §2.2.3, against [`ECDSASecp256K1::verify_der`]). See +//! `wycheproof.rs`'s docs for the sibling-clone convention and what wycheproof's value is here +//! (malleability/invalid-encoding coverage, not signature-generation KATs). + +use bouncycastle_core::signature_key::VerifyingKey; +use bouncycastle_core::traits::SignaturePublicKey; +use bouncycastle_ecdsa::keys_p256k1::ECDSASecp256K1PublicKey; +use bouncycastle_hex::decode as hex_decode; +use serde_json::Value; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +/// Panics, failing the test, if the wycheproof clone is missing: these suites are the only +/// adversarial check on this crate's verification, and a green run that never read them would be +/// indistinguishable from one that did. (For `cargo mutants`, symlink the clone at +/// `/tmp/wycheproof`; its copied tree resolves the relative path there.) +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); these suites require it rather than skipping" + ); +} + +#[test] +fn ecdsa_secp256k1_sha256_p1363_test() { + let contents = get_test_data("ecdsa_secp256k1_sha256_p1363_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-256"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = ECDSASecp256K1PublicKey::from_bytes(&pk_bytes).ok(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_secp256k1_sha256_p1363_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} + +/// The DER counterpart of the above: `ecdsa_secp256k1_sha256_test.json`, against +/// [`ECDSASecp256K1::verify_der`]. +#[test] +fn ecdsa_secp256k1_sha256_der_test() { + let contents = get_test_data("ecdsa_secp256k1_sha256_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-256"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = ECDSASecp256K1PublicKey::from_bytes(&pk_bytes).ok(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify_der(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_secp256k1_sha256_der_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} diff --git a/crypto/ecdsa/tests/wycheproof_p384.rs b/crypto/ecdsa/tests/wycheproof_p384.rs new file mode 100644 index 000000000..8e0b0e04e --- /dev/null +++ b/crypto/ecdsa/tests/wycheproof_p384.rs @@ -0,0 +1,147 @@ +//! Test against the project wycheproof repo: `ecdsa_secp384r1_sha384_p1363_test.json` (raw `r || +//! s`, against [`ECDSAP384::verify`]) and `ecdsa_secp384r1_sha384_test.json` (DER `SEQUENCE { r, s +//! }`, RFC 3279 §2.2.3, against [`ECDSAP384::verify_der`]). See `wycheproof.rs`'s docs for the +//! sibling-clone convention and what wycheproof's value is here (malleability/invalid-encoding +//! coverage, not signature-generation KATs). + +use bouncycastle_core::signature_key::VerifyingKey; +use bouncycastle_core::traits::SignaturePublicKey; +use bouncycastle_ecdsa::keys_p384::ECDSAP384PublicKey; +use bouncycastle_hex::decode as hex_decode; +use serde_json::Value; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +/// Panics, failing the test, if the wycheproof clone is missing: these suites are the only +/// adversarial check on this crate's verification, and a green run that never read them would be +/// indistinguishable from one that did. (For `cargo mutants`, symlink the clone at +/// `/tmp/wycheproof`; its copied tree resolves the relative path there.) +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); these suites require it rather than skipping" + ); +} + +#[test] +fn ecdsa_secp384r1_sha384_p1363_test() { + let contents = get_test_data("ecdsa_secp384r1_sha384_p1363_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-384"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = ECDSAP384PublicKey::from_bytes(&pk_bytes).ok(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_secp384r1_sha384_p1363_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} + +/// The DER counterpart of the above: `ecdsa_secp384r1_sha384_test.json`, against +/// [`ECDSAP384::verify_der`]. +#[test] +fn ecdsa_secp384r1_sha384_der_test() { + let contents = get_test_data("ecdsa_secp384r1_sha384_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-384"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = ECDSAP384PublicKey::from_bytes(&pk_bytes).ok(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify_der(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_secp384r1_sha384_der_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} diff --git a/crypto/ecdsa/tests/wycheproof_p521.rs b/crypto/ecdsa/tests/wycheproof_p521.rs new file mode 100644 index 000000000..52992693c --- /dev/null +++ b/crypto/ecdsa/tests/wycheproof_p521.rs @@ -0,0 +1,147 @@ +//! Test against the project wycheproof repo: `ecdsa_secp521r1_sha512_p1363_test.json` (raw `r || +//! s`, against [`ECDSAP521::verify`]) and `ecdsa_secp521r1_sha512_test.json` (DER `SEQUENCE { r, s +//! }`, RFC 3279 §2.2.3, against [`ECDSAP521::verify_der`]). See `wycheproof.rs`'s docs for the +//! sibling-clone convention and what wycheproof's value is here (malleability/invalid-encoding +//! coverage, not signature-generation KATs). + +use bouncycastle_core::signature_key::VerifyingKey; +use bouncycastle_core::traits::SignaturePublicKey; +use bouncycastle_ecdsa::keys_p521::ECDSAP521PublicKey; +use bouncycastle_hex::decode as hex_decode; +use serde_json::Value; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +/// Panics, failing the test, if the wycheproof clone is missing: these suites are the only +/// adversarial check on this crate's verification, and a green run that never read them would be +/// indistinguishable from one that did. (For `cargo mutants`, symlink the clone at +/// `/tmp/wycheproof`; its copied tree resolves the relative path there.) +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); these suites require it rather than skipping" + ); +} + +#[test] +fn ecdsa_secp521r1_sha512_p1363_test() { + let contents = get_test_data("ecdsa_secp521r1_sha512_p1363_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-512"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = ECDSAP521PublicKey::from_bytes(&pk_bytes).ok(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_secp521r1_sha512_p1363_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} + +/// The DER counterpart of the above: `ecdsa_secp521r1_sha512_test.json`, against +/// [`ECDSAP521::verify_der`]. +#[test] +fn ecdsa_secp521r1_sha512_der_test() { + let contents = get_test_data("ecdsa_secp521r1_sha512_test.json"); + let doc: Value = serde_json::from_str(&contents).unwrap(); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-512"); + let pk_hex = group["publicKey"]["uncompressed"].as_str().unwrap(); + let pk_bytes = hex_decode(pk_hex).unwrap(); + let pk = ECDSAP521PublicKey::from_bytes(&pk_bytes).ok(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + // Only two of wycheproof's three verdicts are decided here. "acceptable" marks a + // vector that is valid under some readings of the specification and not others (its + // `flags` say which), and whether this implementation should accept such a vector is + // a decision to record per flag, not to default either way -- so an "acceptable" + // vector fails the test until someone makes it. The v1 files this suite reads carry + // none; this is what turns a future upgrade that adds some into a loud failure. + let flags = &test["flags"]; + let expect_valid = match test["result"].as_str().unwrap() { + "valid" => true, + "invalid" => false, + other => panic!( + "tcId {tc_id}: result {other:?} with flags {flags} needs an explicit \ + accept/reject decision in this test" + ), + }; + if expect_valid { + num_valid += 1; + } else { + num_invalid += 1; + } + + let result = pk.as_ref().and_then(|pk| pk.verify_der(&msg, None, &sig).ok()); + assert_eq!( + result.is_some(), + expect_valid, + "tcId {tc_id}: expected valid={expect_valid}, flags {flags}, comment={:?}", + test["comment"] + ); + } + } + + println!( + "ecdsa_secp521r1_sha512_der_test: {num_tests} test cases passed ({num_valid} valid, {num_invalid} invalid)." + ); +} diff --git a/crypto/rsa/Cargo.toml b/crypto/rsa/Cargo.toml new file mode 100644 index 000000000..ae675a3f5 --- /dev/null +++ b/crypto/rsa/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "bouncycastle-rsa" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-ec.workspace = true +bouncycastle-rng.workspace = true +bouncycastle-sha2.workspace = true +bouncycastle-sha3.workspace = true +bouncycastle-utils.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +criterion.workspace = true +serde_json = "1.0" + +[[bench]] +name = "rsa_1024_benches" +harness = false + +[[bench]] +name = "rsa_1536_benches" +harness = false + +[[bench]] +name = "rsa_2048_benches" +harness = false + +[[bench]] +name = "rsa_3072_benches" +harness = false + +[[bench]] +name = "rsa_4096_benches" +harness = false + +[[bench]] +name = "rsa_8192_benches" +harness = false diff --git a/crypto/rsa/benches/rsa_1024_benches.rs b/crypto/rsa/benches/rsa_1024_benches.rs new file mode 100644 index 000000000..82a393d06 --- /dev/null +++ b/crypto/rsa/benches/rsa_1024_benches.rs @@ -0,0 +1,73 @@ +//! Criterion benches for RSA-1024: verification only (see `bouncycastle_rsa::rsa_1024`'s docs -- +//! this crate has no RSA-1024 private key type at all). The public key and signature are Wycheproof +//! `rsa_pkcs1_1024_sig_gen_test.json`'s own genuine `(n, e)` and one real `(msg, sig)` pair, the +//! same provenance `tests/rsa_1024_tests.rs` uses. + +use bouncycastle_core::signature_key::VerifyingKey; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rsa::rsa_1024::{ + RSA1024PublicKey, RSASSA_PKCS1_v1_5_SHA256, RSASSA_PKCS1_v1_5_SHA384, +}; + +fn bench_rsa_1024(c: &mut Criterion) { + let n_sha256: [u64; 16] = [ + 0xd00343468eaacfbf, 0xb7c7044cc202dcca, 0x9686f30f478db649, 0x5179b54951fff6aa, + 0xbabb14f550d5d0dd, 0x5405db7c5c8f4cf6, 0x9816e2eda41fd7b9, 0xb31b6abd805bace9, + 0xb909dd0f4c6014f2, 0x9c8a5810b6d05990, 0x40760d1f23fe9250, 0x90adb011a919575a, + 0x45e48572113cab28, 0xcb9ca9ec12000fc8, 0x91b4fcaf62a14595, 0xac9048a7a4f560af, + ]; + let pk_sha256 = RSA1024PublicKey::new(&n_sha256, 0x10001).unwrap(); + let msg_sha256 = hex_decode("0000000000000000000000000000000000000000").unwrap(); + let sig_sha256: [u8; 128] = hex_decode( + "41339884a9b3940e8488d666bb158063c6a2a2717cae7f564834a876fcbf7098ecf3acbfabf37d38a8e6127b1e313744f1f896e165efdaea0b2e7673867842b9e94db0868ed9a92bcdcb370a4e20ff275c82595e4400a8b9e9f12482f014846b48216f321266ae6ae6338dbcdc41b711e483e6e3e728772e7f9f5ef95c30196b", + ) + .unwrap() + .try_into() + .unwrap(); + + let n_sha384: [u64; 16] = [ + 0x5cfd178f3f07ed01, 0x7c94e28878397cb4, 0x57dbd3d6d0f5b77f, 0x89f8ec36d7f5cae7, + 0x73d399b04ad55c0a, 0x33b063490cf7b670, 0x51996b97d5b25da9, 0x0af3f4b5efec3586, + 0xe60ee26a7b7cfccb, 0xaf86df2283c8e327, 0x7dea6692a1713516, 0xe94e94385dffc5e2, + 0x8cf0173a0a309a1e, 0x3ea5161e5450d16a, 0xba51135f78844d9a, 0xa1d3912e65d994e0, + ]; + let pk_sha384 = RSA1024PublicKey::new(&n_sha384, 0x10001).unwrap(); + let msg_sha384 = hex_decode("0000000000000000000000000000000000000000").unwrap(); + let sig_sha384: [u8; 128] = hex_decode( + "74b9a7b9548a281c5a258520c879e0e64d8a28812a7b6461c6e418e0502b61008a8e535a5b55fb64529a6a6df2f60ef33c1844b27f81532be2bc2992d0eb5e524112da90bf40adefcf206469639ff3895a9826674ee1acbdd623842ab0a9a36d48da13ba17c4ee069254da2ea418d5a8f135e2a414c0654a266d538621917545", + ) + .unwrap() + .try_into() + .unwrap(); + + let mut group = c.benchmark_group("rsa_1024"); + group.bench_function("pkcs1v15_verify_sha256", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk_sha256), + black_box(&msg_sha256), + None, + black_box(&sig_sha256), + ) + .unwrap() + }) + }); + group.bench_function("pkcs1v15_verify_sha384", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk_sha384), + black_box(&msg_sha384), + None, + black_box(&sig_sha384), + ) + .unwrap() + }) + }); + group.finish(); +} + +criterion_group!(benches, bench_rsa_1024); +criterion_main!(benches); diff --git a/crypto/rsa/benches/rsa_1536_benches.rs b/crypto/rsa/benches/rsa_1536_benches.rs new file mode 100644 index 000000000..ac56c3ed1 --- /dev/null +++ b/crypto/rsa/benches/rsa_1536_benches.rs @@ -0,0 +1,103 @@ +//! Criterion benches for RSA-1536: verification only (see `bouncycastle_rsa::rsa_1536`'s docs -- +//! this crate has no RSA-1536 private key type at all). Public keys and signatures are Wycheproof +//! `rsa_pkcs1_1536_sig_gen_test.json`'s own genuine `(n, e)` and real `(msg, sig)` pairs, the same +//! provenance `tests/rsa_1536_tests.rs` uses. + +use bouncycastle_core::signature_key::VerifyingKey; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rsa::rsa_1536::{ + RSA1536PublicKey, RSASSA_PKCS1_v1_5_SHA256, RSASSA_PKCS1_v1_5_SHA384, RSASSA_PKCS1_v1_5_SHA512, +}; + +fn bench_rsa_1536(c: &mut Criterion) { + let n_sha256: [u64; 24] = [ + 0xe58590d069a2a271, 0xb2909211b75f8182, 0x052fd6f0a20f6cde, 0x06d4eb560a8fddff, + 0x703a83c6cbfd949d, 0x553f827a27719c2c, 0x395bdb0865f22ac0, 0x12bdb15dde506945, + 0x16db14f2e1c9b8d3, 0x51702cfaa46fcaf7, 0xe62269e04112c194, 0x7728dcea18bc1193, + 0xab906e57364cac81, 0x852f673d3288544a, 0x9547f55a3d7b1d08, 0x8e6d3a99c058b1c8, + 0x5e2c811a56b5d946, 0x6c9fc8200b205487, 0x7b401487a9c85917, 0x41744b5872ccebc8, + 0x86eb04d51f21ac52, 0xe029a743f0649ae5, 0x6cf4930cf2615140, 0xeb477c90d46bd189, + ]; + let pk_sha256 = RSA1536PublicKey::new(&n_sha256, 0x10001).unwrap(); + let msg = hex_decode("0000000000000000000000000000000000000000").unwrap(); + let sig_sha256: [u8; 192] = hex_decode( + "8d2611d4c79f6b2087ae8bc76610905c361b9fe0a6629388197b4293f9e14ecbeb377206e4c1db35cdc0ab163dc5c51e8a7370a059e9ee8014d18ef0937f7936879d7825c792180a4f10a0d46e0a954f093d703b82bd076dcec0b8a66fc3be9bdf79ce4550c453015dc1c7397ec1bfceed040a4d777915546b9cbcf1eeb13eb71ac49c235e69cb07c315d529442f4863d61b7d5caa5ce07820edf649a9342211a26f8280dab9c5dd11af0752168326f8e8d5e834ddba3bce063f011eccc8f46d", + ) + .unwrap() + .try_into() + .unwrap(); + + let n_sha384: [u64; 24] = [ + 0x0c8a0645adb04d7f, 0x4913d26ff2dd0383, 0xe5567ac4c3edf882, 0x965f5b7b146663f4, + 0x6802e12716431e25, 0x8d39e0d5c93fe29a, 0xb940509dbf238383, 0x84fb9ca6ac2ffd03, + 0x67b11769af1178d2, 0x98fa3b27d4de0a32, 0xec385a5db04f4731, 0x065c58db8b8166aa, + 0x2ed95a2a8cfd33da, 0x821cb5e40dc781e7, 0x3cb9b09f241b0700, 0x30b30fe5ea0d2408, + 0xd7841125d38dd9f4, 0xb0ed9964c7b125da, 0x06d573f19163ae2a, 0xa7716c797d15afe5, + 0xcf19e47fadf98cfd, 0xdc9306f2f3646bbc, 0x9c3c1865cf6beaf5, 0xd1060fe7c6d185f0, + ]; + let pk_sha384 = RSA1536PublicKey::new(&n_sha384, 0x10001).unwrap(); + let sig_sha384: [u8; 192] = hex_decode( + "58c4c42da5eee1b757b31e3362a95d75180c0fee472c431527ff5500fe0b5c1d59968d79e6e41650f013a6b3e37c5a1d79233b818ef76c4ed469a09607becdc58987e6a548610de14ff06899ef284778dd5329a27b85072e8ffd46b63a5e8f7602ddb9fc5a07224c49818fc8057581ea36da033f2b936f0761186c7fd82b474e87d47aa1b7ec610642d3bcb16c59bb70ac68b1f081a9c9248f069474d6ed9b29c669fc40a979cdfc2053e1a3c0cf40efa29c01785323995f1d8f3850c32bdf92", + ) + .unwrap() + .try_into() + .unwrap(); + + let n_sha512: [u64; 24] = [ + 0xb4095eb4bf90b6e9, 0x4b57f8d656600e5f, 0x176e311fe8dbcac2, 0x115b2ec41eb9acc4, + 0xdb3ecfc281f8da29, 0xc93bc5bf6c2504fb, 0x0e7896e87fb399e6, 0x69ae5f32937a207e, + 0x230ddd8c067dae90, 0xed1e42ebfbe70645, 0xa16f8e2300ef7c35, 0x70b29f55dd21d2cb, + 0x7e3413f9891e2f8a, 0xa7e0de9008f3a9a6, 0x4df9f5c38e950175, 0xc8e722c8dedc065a, + 0xfe887a4a1fae6ca8, 0xa96a2bedc3e72738, 0xe2ad09578c749863, 0xffb63f097ed7b5c5, + 0xcea5ba02d99f0807, 0x1cb9a9b7b769d6df, 0xfd4c711aef054e29, 0xd1f33c4d0c3b127c, + ]; + let pk_sha512 = RSA1536PublicKey::new(&n_sha512, 0x10001).unwrap(); + let sig_sha512: [u8; 192] = hex_decode( + "5259fe9566e1bd81952a805384b1657374f33550ee0895b57b7a0869a831270d3b3b2db8e295a4eb98d8eb036183b3d228ad22b8e493c7eb0f9ab00eeab2e086f10f8018a8daaff4858ae745a4d70881a166963c64403216422e18974aa456d8229ef2e43002e0390166630c7fabd14efaa974a9521a72a69e0b446971db077ce80f6dd95cf797b6ed276873bbf6b1ba6a79df8c917c2a3fbbc0e5347b0bb99eec6ad0b6bdb7bc9d3e0a7f6b7b623f2aa9dba1e940676ec5629d39778286f261", + ) + .unwrap() + .try_into() + .unwrap(); + + let mut group = c.benchmark_group("rsa_1536"); + group.bench_function("pkcs1v15_verify_sha256", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk_sha256), + black_box(&msg), + None, + black_box(&sig_sha256), + ) + .unwrap() + }) + }); + group.bench_function("pkcs1v15_verify_sha384", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk_sha384), + black_box(&msg), + None, + black_box(&sig_sha384), + ) + .unwrap() + }) + }); + group.bench_function("pkcs1v15_verify_sha512", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk_sha512), + black_box(&msg), + None, + black_box(&sig_sha512), + ) + .unwrap() + }) + }); + group.finish(); +} + +criterion_group!(benches, bench_rsa_1536); +criterion_main!(benches); diff --git a/crypto/rsa/benches/rsa_2048_benches.rs b/crypto/rsa/benches/rsa_2048_benches.rs new file mode 100644 index 000000000..65878ae5b --- /dev/null +++ b/crypto/rsa/benches/rsa_2048_benches.rs @@ -0,0 +1,281 @@ +//! Criterion benches for RSA-2048: one `bench_function` per (scheme, hash) pairing this size +//! wires up, since each has a different performance profile (SHA-256/384/512 hash different +//! amounts of data per block; PSS additionally draws a fresh salt and runs MGF1/SHAKE). The +//! genuine key is the same one `tests/rsa_2048_pkcs1_v1_5_tests.rs` recovers from Wycheproof's +//! `rsa_pkcs1_2048_sig_gen_test.json` (see that file for the full, sourced provenance). + +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{ + KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SignaturePrivateKey, + SignaturePublicKey, +}; +use bouncycastle_rsa::rsa_2048::{RSA2048KEMPrivateKey, RSA2048KEMPublicKey, RSASVE}; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::rsa_2048::keygen; +use bouncycastle_rsa::rsa_2048::{ + RSA2048PrivateKey, RSA2048PublicKey, RSASSA_PKCS1_v1_5_SHA256, RSASSA_PKCS1_v1_5_SHA384, + RSASSA_PKCS1_v1_5_SHA512, RSASSA_PSS_SHA256, RSASSA_PSS_SHA384, RSASSA_PSS_SHA512, + RSASSA_PSS_SHAKE128, +}; + +const MSG: &[u8] = b"a representative message for benchmarking RSA-2048"; + +fn genuine_key() -> RSA2048PrivateKey { + let p: [u64; 16] = [ + 0x0ea36cfb3a5b18f1, 0x48a6e65332119129, 0x110ad9e7b48a1c93, 0x569156b90113e2e9, + 0xe79813a575cfad9c, 0x69d659d143ec6f17, 0xe81e6bab5ddaa783, 0xbff1c5b80a69f788, + 0x978f6c35814f50ee, 0xe6a289ad4cfbf78f, 0x34d5681e5809d415, 0xbb028bda42eeb5d2, + 0x41c56e4de086b0d5, 0x58b8d1e24f3b55d0, 0xfb5248247d98cb7d, 0xdc431050f782e894, + ]; + let q: [u64; 16] = [ + 0x669f140cfbc20f25, 0xb97bb03677207d95, 0xfd4e06f3ed7299d4, 0x160f90536abc9492, + 0xf5b131f39098f7bc, 0xae8d72c57088d7ab, 0x89b94fbde542aba9, 0x3d3f9880ec47d5e0, + 0x1378a6868af3b7a0, 0x5544070beb057c94, 0x16611debc472fac4, 0xe500ffb79f5b8868, + 0x308a5e32196603b2, 0xea5fb19eb4eabc38, 0x122273ae3222b598, 0xbd1a81e7977f9898, + ]; + let d_p: [u64; 16] = [ + 0x209f33f09515d7c1, 0xb4a9b37656917205, 0x276933bb07e4efb9, 0x8c14019808e00414, + 0x289f96da220711e5, 0xfbbd2923d31532fe, 0xc06b414e61c0e1e7, 0x4c23c4588488961d, + 0x4dc48ae34514759c, 0x9c786961ae3e2c35, 0x497e8d9c650688e0, 0x18bf08472612dbe5, + 0x8885fb161870ee12, 0xf21d7c1479d99d47, 0x9121d91952ffd1c7, 0xa94b528b28f29159, + ]; + let d_q: [u64; 16] = [ + 0xf7597ffb68011d8d, 0x7b3cc538c4bab8c9, 0xa8fa480a81a925af, 0x6d6ede7251a383bf, + 0x8a63f788ce3a0f85, 0x0b920502eb478bc9, 0x7e37e755edfe70d9, 0x9cf9948422a16555, + 0x0d6d9ea1f2ef71fd, 0xf7efa32ea0cb6e00, 0x0629b114ca7f780f, 0xcf51176359654348, + 0x540cdcbd4ad35435, 0x31c02ff1a2bc437c, 0xff2503df78bafed5, 0x3af0e72a933aef09, + ]; + let q_inv: [u64; 16] = [ + 0x552fe4bfce945f7b, 0x67e50c999c67247b, 0xfb54ef17be3b2853, 0x241f5921b5ad3983, + 0x02de5eccd143cf31, 0x74e45f6fcc60f216, 0xafa5428a74f12708, 0x88d42294b6a2759b, + 0xe923e1097c0c562f, 0xc968b48a91c38b5b, 0x933e85179c0320b0, 0x7993d0445f758d51, + 0x9bfc042ee0924b1b, 0x41f956d90fa8a793, 0xee7a87b6483a66ee, 0x2640fbfbcfefb163, + ]; + RSA2048PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("genuine RSA-2048 CRT components must be accepted") +} + +fn bench_rsa_2048(c: &mut Criterion) { + let sk = genuine_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + + let sig_pkcs1_256 = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + let sig_pkcs1_384 = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + let sig_pkcs1_512 = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + let sig_pss_256 = SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + let sig_pss_384 = SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + let sig_pss_512 = SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + let sig_pss_shake128 = + SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + + let mut group = c.benchmark_group("rsa_2048"); + + group.bench_function("pkcs1v15_sign_sha256", |b| { + b.iter(|| { + black_box( + DeterministicSigningKey::::sign_deterministic( + black_box(&sk), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pkcs1v15_verify_sha256", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pkcs1_256), + ) + .unwrap() + }) + }); + group.bench_function("pkcs1v15_sign_sha384", |b| { + b.iter(|| { + black_box( + DeterministicSigningKey::::sign_deterministic( + black_box(&sk), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pkcs1v15_verify_sha384", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pkcs1_384), + ) + .unwrap() + }) + }); + group.bench_function("pkcs1v15_sign_sha512", |b| { + b.iter(|| { + black_box( + DeterministicSigningKey::::sign_deterministic( + black_box(&sk), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pkcs1v15_verify_sha512", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pkcs1_512), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_sha256", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_sha256", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_256), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_sha384", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_sha384", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_384), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_sha512", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_sha512", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_512), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_shake128", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_shake128", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_shake128), + ) + .unwrap() + }) + }); + + // RSASVE (SP 800-56B Rev. 2 §7.2.1) under the same key material, re-read as KEM key types + // through the shared raw encoding. Encapsulation is one public-exponent modexp plus the RNG + // draw; decapsulation is the same CRT exponentiation signing does. + let kem_sk = RSA2048KEMPrivateKey::from_bytes(&sk.encode()).unwrap(); + let kem_pk = RSA2048KEMPublicKey::from_bytes(&pk.encode()).unwrap(); + let (_, kem_ct) = RSASVE::encaps(&kem_pk).unwrap(); + group.bench_function("rsasve_encaps", |b| { + b.iter(|| black_box(RSASVE::encaps_rng(black_box(&kem_pk), &mut rng).unwrap())) + }); + group.bench_function("rsasve_decaps", |b| { + b.iter(|| black_box(RSASVE::decaps(black_box(&kem_sk), black_box(&kem_ct)).unwrap())) + }); + + group.finish(); + + // FIPS 186-5 A.1.3 key generation: rejection sampling, so the run-to-run spread is large; + // ten samples is enough to see the order of magnitude without a multi-minute bench. + let mut keygen_group = c.benchmark_group("rsa_2048_keygen"); + keygen_group.sample_size(10); + keygen_group.bench_function("keygen", |b| { + b.iter(|| black_box(keygen(&mut DefaultRNG::default()).unwrap())) + }); + keygen_group.finish(); +} + +criterion_group!(benches, bench_rsa_2048); +criterion_main!(benches); diff --git a/crypto/rsa/benches/rsa_3072_benches.rs b/crypto/rsa/benches/rsa_3072_benches.rs new file mode 100644 index 000000000..c88256df7 --- /dev/null +++ b/crypto/rsa/benches/rsa_3072_benches.rs @@ -0,0 +1,289 @@ +//! Criterion benches for RSA-3072. See `rsa_2048_benches.rs`'s docs for the rationale; the genuine +//! key is the same one `tests/rsa_3072_tests.rs` recovers from Wycheproof's +//! `rsa_pkcs1_3072_sig_gen_test.json`. + +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{ + KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SignaturePrivateKey, + SignaturePublicKey, +}; +use bouncycastle_rsa::rsa_3072::{RSA3072KEMPrivateKey, RSA3072KEMPublicKey, RSASVE}; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::rsa_3072::keygen; +use bouncycastle_rsa::rsa_3072::{ + RSA3072PrivateKey, RSA3072PublicKey, RSASSA_PKCS1_v1_5_SHA256, RSASSA_PKCS1_v1_5_SHA384, + RSASSA_PKCS1_v1_5_SHA512, RSASSA_PSS_SHA256, RSASSA_PSS_SHA384, RSASSA_PSS_SHA512, + RSASSA_PSS_SHAKE128, +}; + +const MSG: &[u8] = b"a representative message for benchmarking RSA-3072"; + +fn genuine_key() -> RSA3072PrivateKey { + let p: [u64; 24] = [ + 0xb26d973345bc4c5f, 0x23251a1d29962ca9, 0x554fc3f23d6c9046, 0x89ffe73b1401e9b8, + 0xe0b448b454670aca, 0xa73ea5c2413d1da2, 0xe9539bc7a8d3b351, 0x357984fc116af9cb, + 0xa93e40aac908e4e3, 0xae586c1e7f5c52cd, 0x80deaeaab651c7a9, 0xe261a4f7f4505580, + 0xc5a703b2fc28bfcf, 0x14521a6893f3f3c5, 0x0f08d649107f449a, 0xd96ceabb7ee83ce5, + 0x1b2e8a9b2bb69525, 0x383ead851fac07ad, 0x939deb88dff68550, 0xce9fa16a1cc92120, + 0x43604a7f2be2860f, 0xba55f20a964c4e63, 0x0ed9ac8a812545da, 0xf5eca16e0e83696b, + ]; + let q: [u64; 24] = [ + 0x7981610848c55cdd, 0x8bce03c5d339b975, 0xb8227d7b5d76ce8b, 0x6edee1e9d17781db, + 0x1c127ac3c4d0bd59, 0xb8b28f38951d7bee, 0x496086fa1300249a, 0x790e5c34729a8efb, + 0x9bdb81968f4a6d7c, 0x6018fd4ed9a3545f, 0x6ccb168d5b510dbe, 0xe1d845c6553c0a54, + 0x73ec1d97f669e298, 0x8b898025ef470e43, 0x160deb327e8ace01, 0x640191099b355611, + 0xe0ec501ccc94b2b0, 0x4f98c9ee4fdae41b, 0x0082a3bc42af1a14, 0x510a623c2b47a522, + 0x32057f9da6dbefc4, 0x95ad92b6f295d610, 0x19ddbfcfa2d96704, 0xcf25446f59cf5129, + ]; + let d_p: [u64; 24] = [ + 0xeeb10a8531c470ed, 0xbcd9be04cdc9d65c, 0xb684b458e4ab3854, 0x6a2dafd0d3b23a21, + 0xc812cbd3dccc8b35, 0x3e67363a947405c6, 0xa3ce9c7d391bdbb2, 0xf8bd10156b4bd580, + 0x3c1ce3ae99eb37da, 0xcfa5f4771567cc23, 0x35e0be9a437021c1, 0x8a527b7b967be52e, + 0xc649435b483585d6, 0x1dc154ddadf6bc20, 0x69105ecfc1144838, 0x3dad9bdd05d4f6d4, + 0xf95601b3d122be79, 0x92f1eed27a0ada46, 0x152e93f904cfe6e6, 0x5a6e6d9c19e8bdb3, + 0x6c0437d3cb7c843f, 0x370e84e9f5f0f931, 0x514c6940c20eb67b, 0x6357a59679d26801, + ]; + let d_q: [u64; 24] = [ + 0x1a4dbeba87772d29, 0xe51861bbb4e73e73, 0x9e86b2483e9bf22c, 0xc9ebcf4d7c2d6c9f, + 0xbe8fe9def6b7a62c, 0x4e8805a755ac2904, 0x734f44f3e4e88d18, 0x7626602fc90ae694, + 0xde97c88d40fa1ac4, 0x4eeaf1aafe2e1ba5, 0x25cd66f5205b038b, 0x8fa6bb6ae73b182d, + 0x0c85ab03068cbfee, 0x96f0ae48529b490f, 0xbb4cc906ba283d18, 0x4bf60c135a264919, + 0x4c2cf95ce74fe42c, 0xf4a5e8eed2a70c7a, 0x14ecee22e846a7d3, 0xd2a4139a39cec9df, + 0x4ba0e0801d31cbf5, 0x088a7986f6c2b8c0, 0x8bdc0f566f876191, 0x0004dadabfc15b1a, + ]; + let q_inv: [u64; 24] = [ + 0x45b268741fca195c, 0x94eacf955ae5dacd, 0x3ad1a334fd85aa87, 0xe8b01c83f2aec93e, + 0xd05fdc78929fa0cf, 0x0745ab95edda215b, 0xe5c81a2189f11467, 0xeded3f90e0ba4a97, + 0xfbf69baed9510be5, 0x6bee976c25dbaff8, 0x6ad65e9fe6c28038, 0xef6613dd6bf885c1, + 0x594990da705ebdf7, 0x8ee8b716f19429fb, 0x2662db8c85318de4, 0x22481fc74a7a3d62, + 0xfe6c3600d9b8e9a9, 0x744fac4daabf5488, 0xfbee6ec24b75fbf0, 0x8be552c8be44f139, + 0xfb0da96bd423759d, 0xb3443d93e7e8ca62, 0x36fe01b950885ecd, 0x214a1f73130e48b3, + ]; + RSA3072PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("genuine RSA-3072 CRT components must be accepted") +} + +fn bench_rsa_3072(c: &mut Criterion) { + let sk = genuine_key(); + let pk = RSA3072PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + + let sig_pkcs1_256 = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + let sig_pkcs1_384 = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + let sig_pkcs1_512 = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + let sig_pss_256 = SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + let sig_pss_384 = SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + let sig_pss_512 = SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + let sig_pss_shake128 = + SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + + let mut group = c.benchmark_group("rsa_3072"); + + group.bench_function("pkcs1v15_sign_sha256", |b| { + b.iter(|| { + black_box( + DeterministicSigningKey::::sign_deterministic( + black_box(&sk), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pkcs1v15_verify_sha256", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pkcs1_256), + ) + .unwrap() + }) + }); + group.bench_function("pkcs1v15_sign_sha384", |b| { + b.iter(|| { + black_box( + DeterministicSigningKey::::sign_deterministic( + black_box(&sk), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pkcs1v15_verify_sha384", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pkcs1_384), + ) + .unwrap() + }) + }); + group.bench_function("pkcs1v15_sign_sha512", |b| { + b.iter(|| { + black_box( + DeterministicSigningKey::::sign_deterministic( + black_box(&sk), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pkcs1v15_verify_sha512", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pkcs1_512), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_sha256", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_sha256", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_256), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_sha384", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_sha384", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_384), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_sha512", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_sha512", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_512), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_shake128", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_shake128", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_shake128), + ) + .unwrap() + }) + }); + + // RSASVE (SP 800-56B Rev. 2 §7.2.1) under the same key material, re-read as KEM key types + // through the shared raw encoding. Encapsulation is one public-exponent modexp plus the RNG + // draw; decapsulation is the same CRT exponentiation signing does. + let kem_sk = RSA3072KEMPrivateKey::from_bytes(&sk.encode()).unwrap(); + let kem_pk = RSA3072KEMPublicKey::from_bytes(&pk.encode()).unwrap(); + let (_, kem_ct) = RSASVE::encaps(&kem_pk).unwrap(); + group.bench_function("rsasve_encaps", |b| { + b.iter(|| black_box(RSASVE::encaps_rng(black_box(&kem_pk), &mut rng).unwrap())) + }); + group.bench_function("rsasve_decaps", |b| { + b.iter(|| black_box(RSASVE::decaps(black_box(&kem_sk), black_box(&kem_ct)).unwrap())) + }); + + group.finish(); + + // FIPS 186-5 A.1.3 key generation: rejection sampling, so the run-to-run spread is large; + // ten samples is enough to see the order of magnitude without a multi-minute bench. + let mut keygen_group = c.benchmark_group("rsa_3072_keygen"); + keygen_group.sample_size(10); + keygen_group.bench_function("keygen", |b| { + b.iter(|| black_box(keygen(&mut DefaultRNG::default()).unwrap())) + }); + keygen_group.finish(); +} + +criterion_group!(benches, bench_rsa_3072); +criterion_main!(benches); diff --git a/crypto/rsa/benches/rsa_4096_benches.rs b/crypto/rsa/benches/rsa_4096_benches.rs new file mode 100644 index 000000000..bedbe96ce --- /dev/null +++ b/crypto/rsa/benches/rsa_4096_benches.rs @@ -0,0 +1,290 @@ +//! Criterion benches for RSA-4096. See `rsa_2048_benches.rs`'s docs for the rationale; the genuine +//! key is the same one `tests/rsa_4096_tests.rs` recovers from Wycheproof's +//! `rsa_pkcs1_4096_sig_gen_test.json`. `shake256` (not `shake128`) is the SHAKE choice wired up at +//! this modulus size (RFC 8702 SS5). + +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{ + KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SignaturePrivateKey, + SignaturePublicKey, +}; +use bouncycastle_rsa::rsa_4096::{RSA4096KEMPrivateKey, RSA4096KEMPublicKey, RSASVE}; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::rsa_4096::{ + RSA4096PrivateKey, RSA4096PublicKey, RSASSA_PKCS1_v1_5_SHA256, RSASSA_PKCS1_v1_5_SHA384, + RSASSA_PKCS1_v1_5_SHA512, RSASSA_PSS_SHA256, RSASSA_PSS_SHA384, RSASSA_PSS_SHA512, + RSASSA_PSS_SHAKE256, +}; + +const MSG: &[u8] = b"a representative message for benchmarking RSA-4096"; + +fn genuine_key() -> RSA4096PrivateKey { + let p: [u64; 32] = [ + 0x5141acd4afd4771f, 0x5857742b7e4032dc, 0xb7a9b97fe5371396, 0x4b1783c5bc91a6dc, + 0xaf5d0c912d97b728, 0xeb265a7f28b88976, 0x5e993302aa72f11d, 0xc007ad09d76ae22a, + 0x44cbb87b8bec8fd3, 0xf152f15c73b95ac5, 0x8dbdad655fbbad20, 0x59855d0de1c794ac, + 0x4c6060926d878a20, 0xf38860474e44993d, 0x177a91caeef98b13, 0xd1676123544d3def, + 0xc1673063d8487390, 0xfedd55ba2e233a6d, 0x016a5735b883c639, 0xbd813f9fb752805b, + 0x923a7b775b5ffaff, 0xb174e55ca6a12c21, 0x0373077895a6934a, 0x6b725388f54ffea3, + 0x953c09104e1fd2d3, 0x9114f39017871528, 0x33918eb72b786b3c, 0xf43b8626af13d14a, + 0x886a0904eb486140, 0x0037409b81f3a5b3, 0x19e44e26140d2ede, 0xc3c677495c2bd566, + ]; + let q: [u64; 32] = [ + 0xee0745546a6a1c73, 0x8b1050436d882425, 0xdc2e7872ce6fb2b7, 0xf98671ee4899050b, + 0x796a5dd847400804, 0x9ba1f51132355792, 0xc39066a9fc27985b, 0x81f4bca5d15e0962, + 0x6a46cf6dbb6d2ad9, 0xdf3d085ae7693b7e, 0xf58e3c750b6d978a, 0xb52b9000d3a7aab1, + 0xdbb07e23bce1960c, 0x4e291b5c8ddaa6a7, 0x0544a7a7cb3f0792, 0xc695e324fbb11beb, + 0xbded418507f2f175, 0x49fe5f47737ff436, 0xeb37f85b7554d7ad, 0x724d042aef25cea2, + 0xeadbd11dcd2fec7b, 0xb50812705b379f5d, 0xfc50fa98fb67b746, 0x16f2bdf5964b26c7, + 0xcc1c817507859246, 0x6810c2136ad81b16, 0xd7bc78d637e17e0e, 0x6882825347455782, + 0xcdb615d921f5e705, 0x0a982efbe6658e6b, 0x59e96686f7b8752f, 0xc357cf685f9b8d4e, + ]; + let d_p: [u64; 32] = [ + 0x3cd8e91fee6bcdd3, 0x074fdc015ab1c45f, 0x34135987a2288907, 0xf6961d44dfb7505c, + 0x251be32315a85b75, 0xe8982e8493a2ae78, 0xc268493861002977, 0x140fb5806735814e, + 0x35d426d719b12c41, 0xa5f194dbbf374226, 0xa0a6c5314c1a2375, 0x66899d59c04b60c4, + 0x5158d7867dbfec94, 0xec1dde997c00125e, 0x62e5a19b2a71decf, 0x310291892c37e8a2, + 0x223e06c9521f4a45, 0xe4f08e1d5543ed74, 0xcf4dc8de58b9e9ad, 0x9167ad4f122b3263, + 0xf110dbac286712f6, 0x2db237d0f39cda5f, 0xa54b67367c857476, 0x138b37b7b1ed2219, + 0x950d9f4e82aab9bc, 0x90943e671e928e23, 0x44791809c9fcf19f, 0xc70396da054ba478, + 0x77248f6fe00e31bd, 0xf3731b1415d5f2ee, 0xf5242aa6657760cd, 0xa6bbb5460638d2b2, + ]; + let d_q: [u64; 32] = [ + 0xfa935bd36284ea6b, 0x9c255a57d6b3ac0f, 0x35b350881ec52e99, 0xf48d35dd11598957, + 0x61fee5d3763b8cfb, 0xf86f5d1936a161ae, 0x3a8b852507264ae2, 0xe558f90950a572a2, + 0x205aeb15029c95a6, 0x0fa8cf020e960c3b, 0x6a793c66abfb0963, 0xa9c7d7b391d0cbee, + 0x6310f31421e85c6b, 0xc0de89d2edb32796, 0x1cf37af6eab4ed97, 0x1533eacf819c878e, + 0xc700fc7e2a482abe, 0x4d1c62a63d917682, 0x89141558095c3228, 0x029bb05df0b8f121, + 0xa239e00b24ba9b5b, 0x74c3c55d5051f35f, 0x2f45200123860c7d, 0x4fabf224d27abaf1, + 0x0acb80fbf7ed2d86, 0x87db455a8bee03ce, 0x980bbbd1afa8fac6, 0x9d9ee6515ded4930, + 0x4c62a05dae0f744b, 0x69ff90e2839348c9, 0xda141e6d351e42da, 0x178cd58f72bf5118, + ]; + let q_inv: [u64; 32] = [ + 0x5792505b857d3e8d, 0x8a13922b1240c0d2, 0xb5fcb8bf3df1f936, 0x74483606c2cd7ed8, + 0xd40f33bbfd9be093, 0xb5f40ed6632989f5, 0x7f2cc0cd8c8a0661, 0x87630c6b25dddeb5, + 0x7032a5af6ef7c20f, 0x90331600142e6cb9, 0x67b92645b68c6d8f, 0xb63517c7c39843cf, + 0xd50712bf8fc35882, 0x3c421e2447b1a989, 0x7a251afa7aaa6908, 0x43b4de11f99b2654, + 0x1e02735920ef48a3, 0x218fc24da34ce8f0, 0x280446e49b3b9b15, 0x8d96f07411c882bf, + 0x66e283f5d5fb0e6e, 0x0844efd28d853446, 0x5eb4def8dba57b99, 0x474111f92d19f3ab, + 0x748d7b5d94cacd4d, 0xcf634f3a07cea4b7, 0x8dbcca51f4da4379, 0x80774236a54ec9dc, + 0x6a1ef00ee582d3d1, 0xdecfb14ca1e80c8e, 0xc78af5f6c807cc99, 0x484ad86e79415ea3, + ]; + RSA4096PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("genuine RSA-4096 CRT components must be accepted") +} + +fn bench_rsa_4096(c: &mut Criterion) { + let sk = genuine_key(); + let pk = RSA4096PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + + let sig_pkcs1_256 = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + let sig_pkcs1_384 = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + let sig_pkcs1_512 = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + let sig_pss_256 = SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + let sig_pss_384 = SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + let sig_pss_512 = SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + let sig_pss_shake256 = + SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + + let mut group = c.benchmark_group("rsa_4096"); + + group.bench_function("pkcs1v15_sign_sha256", |b| { + b.iter(|| { + black_box( + DeterministicSigningKey::::sign_deterministic( + black_box(&sk), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pkcs1v15_verify_sha256", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pkcs1_256), + ) + .unwrap() + }) + }); + group.bench_function("pkcs1v15_sign_sha384", |b| { + b.iter(|| { + black_box( + DeterministicSigningKey::::sign_deterministic( + black_box(&sk), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pkcs1v15_verify_sha384", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pkcs1_384), + ) + .unwrap() + }) + }); + group.bench_function("pkcs1v15_sign_sha512", |b| { + b.iter(|| { + black_box( + DeterministicSigningKey::::sign_deterministic( + black_box(&sk), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pkcs1v15_verify_sha512", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pkcs1_512), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_sha256", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_sha256", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_256), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_sha384", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_sha384", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_384), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_sha512", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_sha512", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_512), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_shake256", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_shake256", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_shake256), + ) + .unwrap() + }) + }); + + // RSASVE (SP 800-56B Rev. 2 §7.2.1) under the same key material, re-read as KEM key types + // through the shared raw encoding. Encapsulation is one public-exponent modexp plus the RNG + // draw; decapsulation is the same CRT exponentiation signing does. + let kem_sk = RSA4096KEMPrivateKey::from_bytes(&sk.encode()).unwrap(); + let kem_pk = RSA4096KEMPublicKey::from_bytes(&pk.encode()).unwrap(); + let (_, kem_ct) = RSASVE::encaps(&kem_pk).unwrap(); + group.bench_function("rsasve_encaps", |b| { + b.iter(|| black_box(RSASVE::encaps_rng(black_box(&kem_pk), &mut rng).unwrap())) + }); + group.bench_function("rsasve_decaps", |b| { + b.iter(|| black_box(RSASVE::decaps(black_box(&kem_sk), black_box(&kem_ct)).unwrap())) + }); + + group.finish(); +} + +criterion_group!(benches, bench_rsa_4096); +criterion_main!(benches); diff --git a/crypto/rsa/benches/rsa_8192_benches.rs b/crypto/rsa/benches/rsa_8192_benches.rs new file mode 100644 index 000000000..cb20348e8 --- /dev/null +++ b/crypto/rsa/benches/rsa_8192_benches.rs @@ -0,0 +1,306 @@ +//! Criterion benches for RSA-8192. See `rsa_2048_benches.rs`'s docs for the rationale; the genuine +//! key is the same fresh, self-generated one `tests/rsa_8192_tests.rs` uses for its BC Java +//! cross-check (see that file for the full provenance). No PSS-SHAKE variant is benched: none is +//! wired up at this modulus size (see `bouncycastle_rsa::rsa_8192`'s docs for why). + +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{ + KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SignaturePrivateKey, + SignaturePublicKey, +}; +use bouncycastle_rsa::rsa_8192::{RSA8192KEMPrivateKey, RSA8192KEMPublicKey, RSASVE}; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::rsa_8192::{ + RSA8192PrivateKey, RSA8192PublicKey, RSASSA_PKCS1_v1_5_SHA256, RSASSA_PKCS1_v1_5_SHA384, + RSASSA_PKCS1_v1_5_SHA512, RSASSA_PSS_SHA256, RSASSA_PSS_SHA384, RSASSA_PSS_SHA512, +}; + +const MSG: &[u8] = b"a representative message for benchmarking RSA-8192"; + +fn genuine_key() -> RSA8192PrivateKey { + let p: [u64; 64] = [ + 0xa9a87892c469b1e5, 0x038c3f4efcb198ac, 0xed4f14df1b8a1e1f, 0xef2330d78ee73020, + 0xd3093ddaf276904c, 0x94a438d69aeaff23, 0x48dbb3d700ad58ed, 0xbbd763b5346ff916, + 0xea402fb475938e3e, 0xcaed4c45c543aff3, 0xc0954c4255b90f57, 0x2fe6242da64a5099, + 0x3fd54a6b6175342c, 0xdf8dd0187588abbd, 0x4abc11d33376e220, 0x90d3482c7fc1397d, + 0xde925fb5a2cb1e7e, 0x062e99dfbd3c790b, 0x243ef703c0912a1e, 0x91ee31a02ef1a02e, + 0x13b19d3960842877, 0xc6e8036e26c4a339, 0xf2606f45a73314c9, 0x5dfe4c6dd0aa04ee, + 0x1385bfde55204d94, 0x454123829d28200d, 0x18a3d86f516f2a50, 0xcbe5e841cd2fdb17, + 0xc86825aae3031a2c, 0xf4726a6681852b6e, 0x3f16b45a496eee29, 0xd5c4b2319abff492, + 0x71077736745aee55, 0x72586dba7b765bbe, 0x681813f6851efdce, 0x196c13fa6d6781d2, + 0x4712ba3eb0734e43, 0xde2a9a8da6da8340, 0xc2f93a2af8ffbaaa, 0x4db552ab82a577ae, + 0x07c075eb13064f67, 0x818a7d0b31680682, 0xa58320f732d69460, 0xa5922479f4aaf594, + 0x4d545baa4cd73f39, 0xa20c87a2660869c4, 0x8350211511fe7d7a, 0x50174a372234ba89, + 0x6eefd36b20bd1ce7, 0x07d124cb61d25c8b, 0x7356e6380041dcd5, 0x6a7c98caab76211a, + 0x61ff7eaac65f815d, 0x1167dc0b28f418b3, 0x9fc8bf056efe8677, 0xf5ce4481e1670e10, + 0x6ae488da222292de, 0xcc786e194ea84d2a, 0x41b134cb6a8ae4f5, 0x870ff94b064466b7, + 0xc82ad608c14a9385, 0x5338550ac9d3f96d, 0xdce87e0d41f1ba9d, 0xef9beedb39920589, + ]; + let q: [u64; 64] = [ + 0x08798e2dd82b7d9f, 0x30208e668897171d, 0x8f73c28d70144ec6, 0x66f64ce7d2402ae0, + 0xdbc26c601d5cc3a3, 0x657176891201beee, 0x692812d7b9b796c5, 0xae27fa9062406a23, + 0x7b4e6ee5b4d2d22f, 0xd846e0e8fbeb0108, 0x52ca75d6866b780b, 0x2620152d7bb490c0, + 0xdca773f826ba9118, 0xb308499a61d0a4eb, 0x44ba0f58bff07ed5, 0x268483fe5829eb5c, + 0x02e7f0c1d3ee3856, 0x76eae3a004d0f37a, 0x99d9cb3aab5b3f1f, 0x0c9f76678a44f2c4, + 0xc82ba3f75883e512, 0x57719c85e8888746, 0x63c967ca1b81cf17, 0x6f4942a86e96bb6a, + 0x614105324fdb77ae, 0xdb6a0c777f7a4bbc, 0x184bf293127472d5, 0xc26798126a967a8f, + 0x45fa10ccadd5ef6e, 0x13cacbbf5cbc2722, 0x23b4e2f2d18785fd, 0x83a4d39485ad254b, + 0x3a096f5ddd674174, 0x6df8f2ed81ae9e7e, 0x77ee2d29fb78201d, 0x4b4c10c9ac8853eb, + 0x35690c51d0ff6208, 0x8de9a271cb50aa59, 0x51b0701f8cdd1505, 0x4cb715c9b578c63b, + 0x3c9b35776d94c3c7, 0xe952d6a7c34a2abb, 0xc432f1fcc2a5105f, 0x4f7f23a62cea2f81, + 0xfa507889f33eedd9, 0x7e37e435cb776085, 0x576b2ad8a4f39820, 0x0a85d1466d55ad69, + 0xb1dee047621be714, 0x216c6b3c1c945527, 0xe6298f6e1575e39e, 0xdd16506432adc399, + 0xc1c065319c823d2e, 0xf2d6f25d6f34a582, 0x75d6801f595c50fc, 0xccfcd556c20e1130, + 0xa24e6a1396173d85, 0xe2cbc5218657c80a, 0xe7bec5bf5a944815, 0x656f5fa54bf06f6c, + 0x7b68b002b0ea62d7, 0xa2eb84753d291781, 0xdd329e1ce00c2087, 0xcdb932d13e38ee0a, + ]; + let d_p: [u64; 64] = [ + 0x848c4d06bcab4c19, 0x2fa349116ae2bfd6, 0xcf9ea12f426f7eb3, 0xfa71d15136d85dff, + 0xc3e3a026e3968f1f, 0xd50f8056de82a33e, 0x68213465c1b3b2d7, 0x426387f62ee9c62e, + 0xeb14efe18920a873, 0xf3c385a60c9dc86c, 0x294ffd8852c4d95e, 0x940d14b53927fef5, + 0x3f709e3b4c8390c3, 0x3b9737d39c42994b, 0x5c02182c4dac78fe, 0x3b6b7c01fe0a0737, + 0x7adeedf6194f6d2c, 0x43c30f449d8ae36c, 0xb88cebf30dac95d4, 0x09fc3d623b8ceb6b, + 0x5d28293b759f2b09, 0xd350dee095846993, 0x1b323dc92b794164, 0xe19d52f0e7ef34bb, + 0x5ff017da15232c02, 0x58fa8d3b24b82973, 0x1c8aed916227e53f, 0xad49cd8938c909d4, + 0xc3d98be0501534a1, 0x91cec83019bd5bb6, 0x985b814d838ffdd2, 0x11df5cb357c86888, + 0xbc0d5590336b8dcf, 0xc25fa85b1912ac66, 0x390c8752384cadef, 0x07b54e62df65825a, + 0x7f7ad57e592cfc3b, 0xed1b7f53d6fe2a09, 0x41d9e577118adb13, 0xb6706780ace45139, + 0xbfbaadef8d19443b, 0x3dc42a1dccca1d87, 0xdb8a72a8ca8d4de7, 0xe10dec18507dbc43, + 0x66343911cfeb0313, 0xd29def907cf51b72, 0x998add714f12ef6f, 0x1c394a4b8053f8ec, + 0x46e0b76d81a07852, 0x498c3f81cb9f2b8d, 0x8fd0019a024e19be, 0xf2d405423b1d94e4, + 0x29ce52d9c49ff544, 0xd72643a03a2f9053, 0x1be76a355cfa02d2, 0x8db921cec169edf6, + 0x728f91a0f7d9dc15, 0xe34e3a4f3d41d26d, 0xab0a691f05332eb8, 0xb6241cc75fd5ee1b, + 0x5b8582afd5c1acda, 0xe405bceba411da3e, 0xd849aacac396ef0e, 0x57b5a6ebccd92cf9, + ]; + let d_q: [u64; 64] = [ + 0x77a312e8aa49ed35, 0x3bdc007b0396dd44, 0x187af52b1d2d6e88, 0x7d84fae3e1b134fa, + 0xb1af50faa281bf7d, 0xf868401279f57864, 0x1dba1e456f88eb01, 0xe2ee28d0f43ba792, + 0x3450781c72a68626, 0x30757ab4889c0c41, 0x7f34c53326389381, 0xa28e969fda4bc5e4, + 0x0ed664e6b7a53062, 0xfc9ad2c9c2bd3cf2, 0x1a7bb5627d58e8ba, 0x68b8c4da5a5a5104, + 0x5a0e586f0ebe7967, 0x2dcff7ca08c9b702, 0x89a724facd778478, 0x027fe07b2ad90a40, + 0xd2f67b0b487433d5, 0x28d451f2f0eaa3ad, 0xee8d5d55700c24ca, 0x584202d4e805a701, + 0xa1c40f0ed77d96a5, 0xc4055e480f1674ba, 0xf9fb2aeb561bf104, 0x6b846fe9ef936161, + 0x128295b5f01cf2ad, 0x4aaff225e483b5dd, 0x7cba43b61174a21c, 0xf68f9c7b4474bcbd, + 0xc8979d3dd627346e, 0x9c2b543e42af83ed, 0x221d3779bb596a50, 0xac4d7c5545e7c88b, + 0xa44c83a4cb2ffe15, 0x414d71c3a67b3563, 0xe22dd5c34f66bf5e, 0x6a9cd6374f4513e7, + 0x27765c4e3c8ee4d5, 0xf632603a9e123d3f, 0x748b0b2d39fcb6bd, 0x0bde2fc8edf891e6, + 0xded2b9cf10705222, 0xe5ca5d358eb8067a, 0x23368f3218b1bd0c, 0xe41d6519183900d0, + 0x36d90382237f0987, 0x858624f4663a543a, 0x2309265e924e5f4c, 0xfc548ae0263785e8, + 0xe010d5d5e76f3406, 0x963cc28c4228ff36, 0x4245a573c17ac537, 0x7e9ab38877e4d606, + 0xc69b8cfcee4fba17, 0xe127881203d02766, 0xf3b33254ff10a7b7, 0x4df7b087512a53dd, + 0x28ffecc1c7a3ee5d, 0x4c693f99aa4b22b7, 0x498cdaef4c3d1223, 0x78c24ba834042b07, + ]; + let q_inv: [u64; 64] = [ + 0xd87df1de9c759f36, 0x3ee1b8b22c8eab2d, 0xd6098595c2696717, 0xa922e4722b8304de, + 0x424bec1c55c1db54, 0x918f2f8bf0ac261a, 0x1e190b2089185564, 0xc0dedbfc6ee606bb, + 0x1d1d701caca565c9, 0x2d735f7a1c5b9d67, 0x8339684f656205db, 0xc2dc06896727d9ce, + 0x6c8d6fe334141d31, 0x5245e157c87cd8b4, 0x4975b68eb2eaad9a, 0xfbc3389ffb0e788d, + 0x5e9a45250d135847, 0x676e7e6a128cfa88, 0x74341d8b28dcbcb8, 0x8f7d07f37a0490e0, + 0x83a0eae9ecc99a0e, 0xa8188892180dc052, 0x3cd79e1eea7809cb, 0x3bcc1d769d917b24, + 0x1d8dc8edc7268e8d, 0x3ce2707055f522ca, 0x163ead0ca2eb5fee, 0xf147fb695aa70022, + 0xab406a6c89161f7d, 0x8fbbfe117a95f46e, 0xa16d4e65396165bd, 0xb54f52ab1bb357e5, + 0x67cdec7b6a349ed9, 0x303875e0d97cf3fa, 0x0af9378ecb114fd8, 0x4ada6f45b9d6b1df, + 0x9ef2cf55de53d0ed, 0x33e4c2070b165b52, 0x3feddf0f7a1158c0, 0x91463a2b1952e83a, + 0x65a9db73de55ef6b, 0x8e18c716a69a9a6a, 0x0ca44356ca6e2898, 0x7df214b19f2114f1, + 0x9af2aee9d5cf1077, 0x22fc8b019fd3d0ff, 0x50c6eead29ab18cd, 0xa7e6621f8eae45fe, + 0x588bde2d99b8692d, 0x339be0a07b4f0a7b, 0xcbdbdcc8717105d2, 0x78bbd3e3392a503e, + 0xb868f9db1cf14f74, 0x52f3dc7f028c9f7e, 0xa3629e034b453834, 0x52bab134f3ae91a2, + 0xd1aac2fe108f98c0, 0x1e799537a42b3060, 0xdbcce6fc287e449b, 0x1985969e5afbc524, + 0x61ba61f820f83597, 0x65c8523f0767bccc, 0xde04e1644a214f9a, 0x9748f8d853be7131, + ]; + RSA8192PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("genuine RSA-8192 CRT components must be accepted") +} + +fn bench_rsa_8192(c: &mut Criterion) { + let sk = genuine_key(); + let pk = RSA8192PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + + let sig_pkcs1_256 = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + let sig_pkcs1_384 = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + let sig_pkcs1_512 = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + let sig_pss_256 = + SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + let sig_pss_384 = + SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + let sig_pss_512 = + SigningKey::::sign(&sk, &mut rng, MSG, None).unwrap(); + + let mut group = c.benchmark_group("rsa_8192"); + + group.bench_function("pkcs1v15_sign_sha256", |b| { + b.iter(|| { + black_box( + DeterministicSigningKey::::sign_deterministic( + black_box(&sk), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pkcs1v15_verify_sha256", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pkcs1_256), + ) + .unwrap() + }) + }); + group.bench_function("pkcs1v15_sign_sha384", |b| { + b.iter(|| { + black_box( + DeterministicSigningKey::::sign_deterministic( + black_box(&sk), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pkcs1v15_verify_sha384", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pkcs1_384), + ) + .unwrap() + }) + }); + group.bench_function("pkcs1v15_sign_sha512", |b| { + b.iter(|| { + black_box( + DeterministicSigningKey::::sign_deterministic( + black_box(&sk), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pkcs1v15_verify_sha512", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pkcs1_512), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_sha256", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_sha256", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_256), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_sha384", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_sha384", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_384), + ) + .unwrap() + }) + }); + group.bench_function("pss_sign_sha512", |b| { + b.iter(|| { + black_box( + SigningKey::::sign( + black_box(&sk), + &mut DefaultRNG::default(), + black_box(MSG), + None, + ) + .unwrap(), + ) + }) + }); + group.bench_function("pss_verify_sha512", |b| { + b.iter(|| { + VerifyingKey::::verify( + black_box(&pk), + black_box(MSG), + None, + black_box(&sig_pss_512), + ) + .unwrap() + }) + }); + + // RSASVE (SP 800-56B Rev. 2 §7.2.1) under the same key material, re-read as KEM key types + // through the shared raw encoding. Encapsulation is one public-exponent modexp plus the RNG + // draw; decapsulation is the same CRT exponentiation signing does. + let kem_sk = RSA8192KEMPrivateKey::from_bytes(&sk.encode()).unwrap(); + let kem_pk = RSA8192KEMPublicKey::from_bytes(&pk.encode()).unwrap(); + let (_, kem_ct) = RSASVE::encaps(&kem_pk).unwrap(); + group.bench_function("rsasve_encaps", |b| { + b.iter(|| black_box(RSASVE::encaps_rng(black_box(&kem_pk), &mut rng).unwrap())) + }); + group.bench_function("rsasve_decaps", |b| { + b.iter(|| black_box(RSASVE::decaps(black_box(&kem_sk), black_box(&kem_ct)).unwrap())) + }); + + group.finish(); +} + +criterion_group!(benches, bench_rsa_8192); +criterion_main!(benches); diff --git a/crypto/rsa/src/codec.rs b/crypto/rsa/src/codec.rs new file mode 100644 index 000000000..1b37d9596 --- /dev/null +++ b/crypto/rsa/src/codec.rs @@ -0,0 +1,33 @@ +//! Big-endian byte-string <-> little-endian limb-array conversion: RFC 8017 §4's OS2IP/I2OSP, +//! specialized to this crate's fixed-width keys (every modulus size this crate offers is a whole +//! number of 64-bit limbs, so the byte length is always exactly `8 * L`) rather than OS2IP's +//! arbitrary-length octet string. `BYTES = 8 * L` is a separate const parameter rather than a +//! computed one for the same reason [`bouncycastle_ec::montgomery`]'s `L2 = 2 * L` is: stable +//! Rust cannot express one const generic as a function of another. +//! +//! Fixed arrays throughout, not slices with a runtime length check: every caller in this crate +//! knows `BYTES` at compile time (it is the modulus's own byte length), so there is nothing to +//! validate at a boundary here -- both directions are infallible. + +/// OS2IP (RFC 8017 §4.2): a big-endian byte string into little-endian limbs. +pub fn limbs_from_be_bytes(bytes: &[u8; BYTES]) -> [u64; L] { + debug_assert_eq!(BYTES, 8 * L, "limbs_from_be_bytes needs BYTES == 8 * L"); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = BYTES - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().expect("8-byte slice")); + } + limbs +} + +/// I2OSP (RFC 8017 §4.1): little-endian limbs into a big-endian byte string. I2OSP's "integer too +/// large" error cannot occur here: an `[u64; L]` is always `< 256^BYTES`. +pub fn be_bytes_from_limbs(limbs: &[u64; L]) -> [u8; BYTES] { + debug_assert_eq!(BYTES, 8 * L, "be_bytes_from_limbs needs BYTES == 8 * L"); + let mut bytes = [0u8; BYTES]; + for i in 0..L { + let start = BYTES - (i + 1) * 8; + bytes[start..start + 8].copy_from_slice(&limbs[i].to_be_bytes()); + } + bytes +} diff --git a/crypto/rsa/src/emsa_pkcs1_v1_5.rs b/crypto/rsa/src/emsa_pkcs1_v1_5.rs new file mode 100644 index 000000000..f52a13a80 --- /dev/null +++ b/crypto/rsa/src/emsa_pkcs1_v1_5.rs @@ -0,0 +1,447 @@ +//! EMSA-PKCS1-v1_5 (RFC 8017 §9.2): deterministic signature encoding, generic over the hash +//! function `H` via its [`HashAlgParams`]/[`AlgorithmOID`] impls (e.g. +//! `bouncycastle_sha2::SHA256`), and specialized to this crate's fixed-width keys: RFC 8017's +//! `emLen` is always this crate's modulus byte length `K_LEN` (`8 * L`), never a generic +//! caller-chosen length. Both operations take the message's already-computed hash rather than +//! the message (step 1 of the encoding is the caller's), so that a streaming signer/verifier can +//! hash incrementally and hand in the result; `H` is never instantiated here. + +use bouncycastle_core::traits::{AlgorithmOID, HashAlgParams}; + +/// `T`, the DER encoding of `DigestInfo { digestAlgorithm, digest }` (RFC 8017 §9.2 step 2): +/// +/// ```text +/// DigestInfo ::= SEQUENCE { digestAlgorithm AlgorithmIdentifier, digest OCTET STRING } +/// AlgorithmIdentifier ::= SEQUENCE { algorithm OBJECT IDENTIFIER, parameters NULL } +/// ``` +/// +/// Built directly from `H::OID_DER` (already the DER encoding of the `OBJECT IDENTIFIER` itself, +/// tag and length included) rather than a hand-copied table of the nine prefixes RFC 8017 §9.2 +/// note 1 lists for MD2/MD5/SHA-1/SHA-224/SHA-256/SHA-384/SHA-512/SHA-512-224/SHA-512-256: +/// wrapping `OID_DER` in the two surrounding `SEQUENCE`s and the `05 00` (`NULL`) parameters, then +/// appending the digest behind its own `04 ` (`OCTET STRING`) tag, reproduces exactly those +/// bytes -- checked against SHA-256/384/512 in this module's tests. Every DER length here +/// (`H::OID_DER.len()` and `H_LEN`) is well under 128 for every hash this crate could plausibly +/// support (SHA-3's largest output is 64 bytes), so each is a single length byte, not the +/// multi-byte long form; `debug_assert`'d, since it can only fail on a hash this crate does not +/// support, never on real input. +fn digest_info( + digest: &[u8; H_LEN], +) -> [u8; T_LEN] { + debug_assert_eq!(H_LEN, H::OUTPUT_LEN, "digest_info: H_LEN must match H::OUTPUT_LEN"); + let oid_der = H::OID_DER; + debug_assert!(oid_der.len() < 128 && H_LEN < 128, "DER length must fit in one byte"); + + let inner_len = oid_der.len() + 2; // + 05 00 (the AlgorithmIdentifier's NULL parameters) + let outer_len = 4 + inner_len + H_LEN; // + "30 " and "04 " + debug_assert_eq!(T_LEN, 2 + outer_len, "digest_info: T_LEN must match the computed DER length"); + + let mut t = [0u8; T_LEN]; + t[0] = 0x30; + t[1] = outer_len as u8; + t[2] = 0x30; + t[3] = inner_len as u8; + t[4..4 + oid_der.len()].copy_from_slice(oid_der); + let mut i = 4 + oid_der.len(); + t[i] = 0x05; + t[i + 1] = 0x00; + t[i + 2] = 0x04; + t[i + 3] = H_LEN as u8; + i += 4; + t[i..i + H_LEN].copy_from_slice(digest); + t +} + +/// EMSA-PKCS1-V1_5-ENCODE (RFC 8017 §9.2) from step 2 onward, with `emLen` fixed to `K_LEN` (see +/// the module docs) and step 1's `H = Hash(M)` supplied by the caller as `digest`: the caller is +/// either [`crate::rsassa_pkcs1_v1_5::sign`] hashing a whole message in one call, or a streaming +/// `Signer` that hashed it incrementally -- this function itself never sees the message. +/// +/// Step 2 wraps `digest` in [`digest_info`]. Step 3's error ("intended encoded message length +/// too short", `emLen < tLen + 11`) is a `debug_assert` here, not a runtime error: every (hash, +/// modulus size) pairing this crate actually wires up has a modulus thousands of bits wider than +/// any hash's `DigestInfo`, so it can only fire on a deliberately-wrong generic instantiation, +/// never on real input. Steps 4-5 build `EM = 0x00 || 0x01 || PS || 0x00 || T`, where `PS` is +/// `K_LEN - T_LEN - 3` bytes of `0xff` (at least 8, per step 3's bound). +pub fn emsa_pkcs1_v1_5_encode_from_hash< + H: HashAlgParams + AlgorithmOID, + const H_LEN: usize, + const T_LEN: usize, + const K_LEN: usize, +>( + digest: &[u8; H_LEN], +) -> [u8; K_LEN] { + let t: [u8; T_LEN] = digest_info::(digest); + + debug_assert!(K_LEN >= T_LEN + 11, "emsa_pkcs1_v1_5_encode: modulus too short for this hash"); + + let mut em = [0xffu8; K_LEN]; + em[0] = 0x00; + em[1] = 0x01; + em[K_LEN - T_LEN - 1] = 0x00; + em[K_LEN - T_LEN..].copy_from_slice(&t); + em +} + +/// A single DER length octet in strict short form: `None` if the byte's top bit is set (BER's +/// long form, always rejected here -- see [`emsa_pkcs1_v1_5_verify_from_hash`]) or `bytes` doesn't reach +/// `pos`. +fn short_form_len(bytes: &[u8], pos: usize) -> Option { + let len = *bytes.get(pos)? as usize; + if len >= 0x80 { None } else { Some(len) } +} + +/// Checks that `t` is a `DigestInfo` DER encoding (RFC 8017 §9.2 step 2) of `digest` (the +/// message's hash under `H`), tolerating both `AlgorithmIdentifier` shapes: `parameters NULL` +/// (what [`digest_info`] always produces) and `parameters` ABSENT (not produced here, but present +/// in the wild: the `AlgorithmIdentifier`'s `parameters` field is `ANY DEFINED BY algorithm`, and +/// it needs none for a hash OID, so some implementations omit it) -- see +/// [`emsa_pkcs1_v1_5_verify_from_hash`]'s docs for why verification must accept it. Every other +/// check is a strict requirement: a DER length must be single-byte short form, and no byte of `t` +/// may go unaccounted for. +fn decode_digest_info(t: &[u8], digest: &[u8; H_LEN]) -> bool { + let mut pos = 0; + if t.first() != Some(&0x30) { + return false; + } + pos += 1; + let Some(outer_len) = short_form_len(t, pos) else { return false }; + pos += 1; + if pos + outer_len != t.len() { + return false; // T must be exactly outer_len + 2 bytes -- no trailing garbage. + } + if t.get(pos) != Some(&0x30) { + return false; + } + pos += 1; + let Some(inner_len) = short_form_len(t, pos) else { return false }; + pos += 1; + let inner_end = pos + inner_len; + // Mutating this `>` to `>=` is an accepted equivalent, not a gap: the boundary it would + // additionally reject (`inner_end == t.len()`) leaves no room after the AlgorithmIdentifier + // for the OCTET STRING tag a genuine DigestInfo always has, so `t.get(pos) != Some(&0x04)` + // below (reached once `pos` advances to `inner_end`) already rejects it -- one check later, + // same outcome. + if inner_end > t.len() { + return false; + } + let oid = H::OID_DER; + if pos + oid.len() > inner_end || t[pos..pos + oid.len()] != *oid { + return false; + } + pos += oid.len(); + if t.get(pos..pos + 2) == Some(&[0x05, 0x00]) { + pos += 2; // Optional NULL parameters -- see this function's docs. + } + if pos != inner_end { + return false; // Trailing bytes inside AlgorithmIdentifier that aren't a bare NULL. + } + if t.get(pos) != Some(&0x04) { + return false; + } + pos += 1; + let Some(digest_len) = short_form_len(t, pos) else { return false }; + pos += 1; + // Mutating this `||` to `&&` is an accepted equivalent, not a gap: the final `t[pos..]` + // comparison against `digest` (a fixed `[u8; H_LEN]`) already fails whenever either half of + // this condition is true alone -- a `digest_len` that disagrees with `H_LEN` or a + // `pos + digest_len` that disagrees with `t.len()` both show up as a slice/array length + // mismatch there, which `PartialEq` for `[u8]` against `[u8; H_LEN]` treats as unequal without + // panicking. This check exists to fail fast and by a clearer name, not because anything + // downstream depends on it having fired. + if digest_len != H_LEN || pos + digest_len != t.len() { + return false; + } + + t[pos..] == *digest +} + +/// EMSA-PKCS1-v1_5 verification of `em` against the message's hash `digest` (supplied by the +/// caller, as for [`emsa_pkcs1_v1_5_encode_from_hash`]): RFC 8017 §8.2.2's own note describes +/// this as an alternative to its step 4 ("apply a 'decoding' operation ... to recover the +/// underlying hash value, and then compare it to a newly computed hash value") -- required here, +/// rather than optional, because re-encoding the digest and comparing bytes (this crate's +/// [`emsa_pkcs1_v1_5_encode_from_hash`]) only ever produces one canonical `EM`, and +/// [`decode_digest_info`] must accept two. Checks `EM = 0x00 || 0x01 || PS || 0x00 || T` +/// structurally (at least 8 bytes of `0xff` padding, then the separator), then hands `T` to +/// [`decode_digest_info`]. +/// +/// Confirmed against every forgery-shaped case in Wycheproof's `rsa_signature_2048_sha256_test.json` +/// (`BerEncodedPadding`, `InvalidAsnInPadding`, `ModifiedPadding`, `WrongHash`, `InvalidPadding`, +/// `ShortPadding`, `NoHash`), and its one `MissingNull` "acceptable" case. +pub fn emsa_pkcs1_v1_5_verify_from_hash( + digest: &[u8; H_LEN], + em: &[u8; K_LEN], +) -> bool { + if em[0] != 0x00 || em[1] != 0x01 { + return false; + } + let mut i = 2; + while i < K_LEN && em[i] == 0xff { + i += 1; + } + // Mutating this `- 2` to `+ 2` or `/ 2` is an accepted equivalent, not a gap, for every + // concrete `(H, K_LEN)` this crate actually instantiates: `decode_digest_info` requires `T` + // to exactly fill whatever remains of `em` after the separator (no trailing garbage, RFC 8017 + // SS9.2 note 1's structure enforced at line 109 above), which pins `i` to one specific value + // for a genuine, hash-matching `DigestInfo` -- the same value regardless of how `ps_len` is + // computed from it, since `ps_len` is used only for the `< 8` comparison immediately below, + // and that real value of `i` is always thousands of bits' worth of padding away from the + // boundary where an off-by-a-constant miscalculation could flip the comparison's answer. + let ps_len = i - 2; + if ps_len < 8 || i >= K_LEN || em[i] != 0x00 { + return false; + } + + decode_digest_info::(&em[i + 1..], digest) +} + +#[cfg(test)] +mod tests { + //! `digest_info` is crate-private (only [`emsa_pkcs1_v1_5_encode_from_hash`] needs it + //! directly), so it is exercised here rather than from `tests/` -- the same "high-risk code + //! that cannot be reached through the public API" exception `rsa_core`'s tests use, though + //! `digest_info` itself is not high-risk so much as inconvenient to reach any other way. The + //! expected DER prefixes are RFC 8017 §9.2 note 1's own literal byte strings for + //! SHA-256/384/512. + + use super::*; + use bouncycastle_core::traits::HashOneShot; + use bouncycastle_sha2::{SHA256, SHA384, SHA512}; + + /// The whole EMSA-PKCS1-V1_5-ENCODE over a message, SHA-256: step 1 here, the rest in + /// [`emsa_pkcs1_v1_5_encode_from_hash`] -- what `rsassa_pkcs1_v1_5::sign` does, restated + /// locally so these tests can speak in terms of messages. + fn encode(message: &[u8]) -> [u8; K_LEN] { + let mut digest = [0u8; 32]; + SHA256::hash_out(message, &mut digest); + emsa_pkcs1_v1_5_encode_from_hash::(&digest) + } + + /// [`encode`]'s verification counterpart, SHA-256. + fn verify(message: &[u8], em: &[u8; K_LEN]) -> bool { + let mut digest = [0u8; 32]; + SHA256::hash_out(message, &mut digest); + emsa_pkcs1_v1_5_verify_from_hash::(&digest, em) + } + + #[test] + fn digest_info_matches_rfc8017_sha256_prefix() { + let digest = [0xabu8; 32]; + let t = digest_info::(&digest); + let mut expected = [0u8; 51]; + expected[..19].copy_from_slice(&[ + 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 0x01, 0x05, 0x00, 0x04, 0x20, + ]); + expected[19..].copy_from_slice(&digest); + assert_eq!(t, expected); + } + + #[test] + fn digest_info_matches_rfc8017_sha384_prefix() { + let digest = [0xcdu8; 48]; + let t = digest_info::(&digest); + let mut expected = [0u8; 67]; + expected[..19].copy_from_slice(&[ + 0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 0x02, 0x05, 0x00, 0x04, 0x30, + ]); + expected[19..].copy_from_slice(&digest); + assert_eq!(t, expected); + } + + #[test] + fn digest_info_matches_rfc8017_sha512_prefix() { + let digest = [0xefu8; 64]; + let t = digest_info::(&digest); + let mut expected = [0u8; 83]; + expected[..19].copy_from_slice(&[ + 0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, + 0x03, 0x05, 0x00, 0x04, 0x40, + ]); + expected[19..].copy_from_slice(&digest); + assert_eq!(t, expected); + } + + #[test] + fn encode_layout_is_00_01_ff_dot_dot_dot_00_t() { + let em = encode::<256>(b"hello"); + assert_eq!(em[0], 0x00); + assert_eq!(em[1], 0x01); + assert!(em[2..256 - 51 - 1].iter().all(|&b| b == 0xff)); + assert_eq!(em[256 - 51 - 1], 0x00); + assert_eq!(&em[256 - 51..256 - 51 + 19][..2], &[0x30, 0x31]); + } + + #[test] + fn encode_is_deterministic() { + let a = encode::<256>(b"same message"); + let b = encode::<256>(b"same message"); + assert_eq!(a, b); + } + + #[test] + fn encode_differs_for_different_messages() { + let a = encode::<256>(b"message one"); + let b = encode::<256>(b"message two"); + assert_ne!(a, b); + } + + #[test] + fn verify_accepts_what_encode_produces() { + let em = encode::<256>(b"hello"); + assert!(verify::<256>(b"hello", &em)); + } + + #[test] + fn verify_rejects_wrong_message() { + let em = encode::<256>(b"hello"); + assert!(!verify::<256>(b"goodbye", &em)); + } + + /// The `MissingNull` case Wycheproof's `rsa_signature_2048_sha256_test.json` marks + /// "acceptable": a `DigestInfo` whose `AlgorithmIdentifier` omits the `NULL` parameters this + /// crate's own [`emsa_pkcs1_v1_5_encode_from_hash`] always includes. + #[test] + fn verify_accepts_digest_info_with_null_parameters_absent() { + let mut digest = [0u8; 32]; + SHA256::hash_out(b"hello", &mut digest); + let oid = ::OID_DER; // 11 bytes: 06 09 <9-byte OID> + // T without "05 00": 30 30 04 20 , i.e. 2 bytes + // shorter than digest_info's own (NULL-including) T for the same hash. + let mut t = [0u8; 49]; + t[0] = 0x30; + t[1] = 47; + t[2] = 0x30; + t[3] = oid.len() as u8; + t[4..4 + oid.len()].copy_from_slice(oid); + let mut i = 4 + oid.len(); + t[i] = 0x04; + t[i + 1] = 32; + i += 2; + t[i..i + 32].copy_from_slice(&digest); + + let mut em = [0xffu8; 256]; + em[0] = 0x00; + em[1] = 0x01; + em[256 - 49 - 1] = 0x00; + em[256 - 49..].copy_from_slice(&t); + + assert!(verify::<256>(b"hello", &em)); + } + + /// Wycheproof's `BerEncodedPadding`: a `DigestInfo` length re-encoded in BER long form (top + /// bit set) rather than DER's required single-byte short form -- must be rejected outright, + /// since accepting any non-canonical length encoding is exactly the class of leniency + /// Bleichenbacher-style forgeries exploit, regardless of what value it happens to encode. + #[test] + fn verify_rejects_ber_long_form_length() { + let mut em = encode::<256>(b"hello"); + let t_start = 256 - 51; + assert_eq!(em[t_start], 0x30); + assert_eq!(em[t_start + 1], 49); // outer_len + em[t_start + 1] |= 0x80; + assert!(!verify::<256>(b"hello", &em)); + } + + /// Wycheproof's `InvalidAsnInPadding`: a `DigestInfo` length off by one from the true value. + #[test] + fn verify_rejects_wrong_der_length() { + let mut em = encode::<256>(b"hello"); + let t_start = 256 - 51; + em[t_start + 1] += 1; + assert!(!verify::<256>(b"hello", &em)); + } + + /// Wycheproof's `ModifiedPadding` ("appending 0's to digestInfo"): two extra zero bytes appended + /// after an otherwise-valid `DigestInfo`, with `PS` shrunk by two bytes to keep `EM`'s total + /// length fixed. A check that only confirms `EM` *starts with* the right bytes would miss + /// this; requiring every byte of `T` to be accounted for by the parse does not. + #[test] + fn verify_rejects_trailing_garbage_after_digest_info() { + let good = encode::<256>(b"hello"); + let mut em = [0xffu8; 256]; + em[0] = 0x00; + em[1] = 0x01; + em[256 - 51 - 2 - 1] = 0x00; // PS is 2 bytes shorter than the valid encoding's. + em[256 - 51 - 2..256 - 2].copy_from_slice(&good[256 - 51..]); // the valid T, unmodified. + em[256 - 2..].copy_from_slice(&[0x00, 0x00]); // two bytes appended after it. + assert!(!verify::<256>(b"hello", &em)); + } + + /// Wycheproof's `ShortPadding`: fewer than the required 8 `0xff` bytes. + #[test] + fn verify_rejects_short_padding() { + let mut em = [0u8; 256]; + em[0] = 0x00; + em[1] = 0x01; + em[2] = 0x00; // PS has length 0, not >= 8 + assert!(!verify::<256>(b"hello", &em)); + } + + /// Wycheproof's `WrongHash`: the OID in the padding names a different hash than the one used + /// to verify. + #[test] + fn verify_rejects_wrong_hash_oid() { + let mut em = encode::<256>(b"hello"); + let t_start = 256 - 51; + em[t_start + 4] ^= 0xff; // flip a byte inside the OID + assert!(!verify::<256>(b"hello", &em)); + } + + /// A crafted `EM` whose `DigestInfo` claims an `AlgorithmIdentifier` length long enough to run + /// past the end of the (very short, in this construction) remaining buffer. `decode_digest_info` + /// must reject this cleanly via its `inner_end > t.len()` check (line 119) rather than let a + /// later, unguarded slice (`t[pos..pos + oid.len()]`) index past the real buffer and panic: + /// `pos + oid.len()` is fixed (4 + 11 = 15 for SHA-256) and does not itself depend on the + /// claimed length, so nothing downstream catches an inflated `inner_end` before that slice + /// runs -- this early check is the one thing standing between a malformed signature and a + /// crash, not merely a spec-conformance nicety. RSA verification exists to be run on + /// adversarial input, so a signature that panics the verifier is a real (if effortful to find, + /// since the attacker only controls `s`, not `s^e mod n` directly) denial-of-service vector. + #[test] + fn verify_rejects_digest_info_length_that_would_overrun_the_buffer_instead_of_panicking() { + let mut em = [0xffu8; 256]; + em[0] = 0x00; + em[1] = 0x01; + em[251] = 0x00; // separator; PS is em[2..251], 249 bytes, comfortably >= 8 + // T = em[252..256], only 4 bytes: 30 02 30 64 -- an AlgorithmIdentifier claiming 100 + // (0x64) content bytes where only 0 remain. + em[252] = 0x30; + em[253] = 0x02; + em[254] = 0x30; + em[255] = 0x64; + assert!(!verify::<256>(b"hello", &em)); + } + + /// RFC 8017 §9.2 step 4: "The length of PS will be at least 8 octets" -- exactly 8 must be + /// accepted, not just "8 or more" in the abstract. `K_LEN = 62` is the minimum modulus width + /// this hash's `DigestInfo` (`T_LEN = 51`) allows at all (the `K_LEN >= T_LEN + 11` bound + /// [`emsa_pkcs1_v1_5_encode_from_hash`]'s docs describe), which pins `PS` to exactly 8 bytes -- the one + /// pairing that distinguishes "`< 8`" from "`<= 8`" or "`== 8`" as the rejection condition. + #[test] + fn verify_accepts_the_minimum_8_byte_ps_boundary() { + let em = encode::<62>(b"hello"); + assert_eq!(em[2..2 + 8], [0xff; 8], "this construction must exercise ps_len == 8 exactly"); + assert!(verify::<62>(b"hello", &em)); + } + + /// RFC 8017 §9.2 step 2: `EM`'s block type byte (`em[1]`) must be exactly `0x01`. `em[0]` + /// stays correct (`0x00`) so this exercises only the block-type half of the check. + #[test] + fn verify_rejects_wrong_block_type() { + let mut em = encode::<256>(b"hello"); + em[1] = 0x02; + assert!(!verify::<256>(b"hello", &em)); + } + + /// RFC 8017 §9.2 step 2: `EM`'s leading byte (`em[0]`) must be exactly `0x00`. `em[1]` stays + /// correct (`0x01`) so this exercises only the leading-byte half of the check. + #[test] + fn verify_rejects_wrong_leading_byte() { + let mut em = encode::<256>(b"hello"); + em[0] = 0x01; + assert!(!verify::<256>(b"hello", &em)); + } +} diff --git a/crypto/rsa/src/emsa_pss.rs b/crypto/rsa/src/emsa_pss.rs new file mode 100644 index 000000000..f5292bee5 --- /dev/null +++ b/crypto/rsa/src/emsa_pss.rs @@ -0,0 +1,249 @@ +//! EMSA-PSS (RFC 8017 §9.1): probabilistic signature encoding, built on [`crate::mgf1`]. +//! +//! # The `8 * K_LEN - emBits == 1` simplification +//! +//! RFC 8017 encodes into `emLen = ceil(emBits / 8)` octets, where `emBits = modBits - 1`, and +//! steps 11/9 clear the leftmost `8 * emLen - emBits` bits of the encoded message's first octet +//! so that `OS2IP(EM) < 2^emBits <= n`. Every modulus size this crate offers is a whole number of +//! 64-bit limbs, so `modBits` (and therefore `emLen = K_LEN`) is always a multiple of 8; that +//! makes `8 * K_LEN - emBits = 8 * K_LEN - (8 * K_LEN - 1) = 1` for every concrete instantiation +//! here, so this module hard-codes "clear the top bit of the first octet" instead of threading a +//! generic bit-count parameter through for a case that cannot arise. + +use crate::mgf1::mgf1; +use bouncycastle_core::traits::HashOneShot; + +/// EMSA-PSS-ENCODE (RFC 8017 §9.1.1) from step 3 onward, with `emBits` fixed to `8 * K_LEN - 1` +/// (see the module docs), steps 1-2's `mHash = Hash(M)` supplied by the caller (either +/// [`crate::rsassa_pss`] hashing a whole message in one call, or a streaming `Signer` that hashed +/// it incrementally -- this function itself never sees the message), and the salt supplied +/// rather than generated here -- [`crate::rsassa_pss`] draws it from an RNG; this function stays +/// deterministic so it can be tested directly against a known salt. Step 3's error ("encoding +/// error", `emLen < hLen + sLen + 2`) is a `debug_assert`, since every (hash, modulus size) +/// pairing this crate wires up has a modulus far wider than any hash-plus-salt combination it +/// offers. +pub fn emsa_pss_encode_from_hash< + H: HashOneShot, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const K_LEN: usize, +>( + m_hash: &[u8; H_LEN], + salt: &[u8; S_LEN], +) -> [u8; K_LEN] { + debug_assert_eq!(M_PRIME_LEN, 8 + H_LEN + S_LEN, "emsa_pss_encode: M_PRIME_LEN mismatch"); + debug_assert_eq!(DB_LEN, K_LEN - H_LEN - 1, "emsa_pss_encode: DB_LEN mismatch"); + debug_assert!( + K_LEN >= H_LEN + S_LEN + 2, + "emsa_pss_encode: modulus too short for this hash/salt" + ); + + // Steps 5-6: M' = 8 zero octets || mHash || salt; H = Hash(M'). + let mut m_prime = [0u8; M_PRIME_LEN]; + m_prime[8..8 + H_LEN].copy_from_slice(m_hash); + m_prime[8 + H_LEN..].copy_from_slice(salt); + let mut h = [0u8; H_LEN]; + H::hash_out(&m_prime, &mut h); + + // Steps 7-8: DB = PS || 0x01 || salt (PS is all-zero, already the array's initial value). + let mut db = [0u8; DB_LEN]; + db[DB_LEN - S_LEN - 1] = 0x01; + db[DB_LEN - S_LEN..].copy_from_slice(salt); + + // Steps 9-11: maskedDB = DB xor MGF(H, DB_LEN), then clear the top bit (see module docs). + let db_mask: [u8; DB_LEN] = mgf1::(&h); + let mut masked_db = [0u8; DB_LEN]; + for i in 0..DB_LEN { + masked_db[i] = db[i] ^ db_mask[i]; + } + masked_db[0] &= 0x7f; + + // Step 12: EM = maskedDB || H || 0xbc. + let mut em = [0u8; K_LEN]; + em[..DB_LEN].copy_from_slice(&masked_db); + em[DB_LEN..DB_LEN + H_LEN].copy_from_slice(&h); + em[K_LEN - 1] = 0xbc; + em +} + +/// EMSA-PSS-VERIFY (RFC 8017 §9.1.2) from step 4 onward, with `emBits` fixed as +/// [`emsa_pss_encode_from_hash`]'s is and step 2's `mHash = Hash(M)` supplied by the caller the +/// same way. Step 1's message-length limit and step 3's minimum-`emLen` check are not reachable +/// for this crate's fixed sizes, for the same reasons given there. +pub fn emsa_pss_verify_from_hash< + H: HashOneShot, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const K_LEN: usize, +>( + m_hash: &[u8; H_LEN], + em: &[u8; K_LEN], +) -> bool { + // Step 4. + if em[K_LEN - 1] != 0xbc { + return false; + } + // Step 6 (checked directly on `em[0]`, which is maskedDB's own leftmost octet). + if em[0] & 0x80 != 0 { + return false; + } + + // Step 5. + let mut masked_db = [0u8; DB_LEN]; + masked_db.copy_from_slice(&em[..DB_LEN]); + let mut h = [0u8; H_LEN]; + h.copy_from_slice(&em[DB_LEN..DB_LEN + H_LEN]); + + // Steps 7-9. + let db_mask: [u8; DB_LEN] = mgf1::(&h); + let mut db = [0u8; DB_LEN]; + for i in 0..DB_LEN { + db[i] = masked_db[i] ^ db_mask[i]; + } + db[0] &= 0x7f; + + // Step 10. + let ps_len = DB_LEN - S_LEN - 1; + if db[..ps_len].iter().any(|&b| b != 0) || db[ps_len] != 0x01 { + return false; + } + + // Step 11. + let mut salt = [0u8; S_LEN]; + salt.copy_from_slice(&db[DB_LEN - S_LEN..]); + + // Steps 12-14 (step 2's mHash is the caller's `m_hash`). + let mut m_prime = [0u8; M_PRIME_LEN]; + m_prime[8..8 + H_LEN].copy_from_slice(m_hash); + m_prime[8 + H_LEN..].copy_from_slice(&salt); + let mut h_prime = [0u8; H_LEN]; + H::hash_out(&m_prime, &mut h_prime); + + h == h_prime +} + +#[cfg(test)] +mod tests { + //! `emsa_pss_encode_from_hash`/`emsa_pss_verify_from_hash` are crate-private (only + //! [`crate::rsassa_pss`] needs them), so they are exercised here rather than from `tests/` -- + //! the same "high-risk code that cannot be reached through the public API" exception + //! `rsa_core`'s tests use. The KAT is a direct Python transliteration of RFC 8017 SS9.1.1's + //! own steps (not from recall), reusing this crate's own `mgf1` reference values (`mgf1.rs`'s + //! own tests) for the mask. The `encode`/`verify` helpers below add step 2's `mHash = Hash(M)` + //! back on top, so the tests can speak in terms of messages. + + use super::*; + use bouncycastle_sha2::SHA256; + + const SALT: [u8; 32] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, + ]; + const EXPECTED_EM: [u8; 256] = [ + 0x62, 0x2c, 0x85, 0xcc, 0xb1, 0xe5, 0x9b, 0xb7, 0x46, 0x99, 0x27, 0xba, 0xab, 0xa2, 0x37, + 0x72, 0xd6, 0x77, 0xe5, 0xd9, 0x2d, 0x4c, 0x40, 0xc8, 0x08, 0x2c, 0x00, 0xbc, 0xed, 0x77, + 0x17, 0x75, 0x34, 0x88, 0x46, 0xd1, 0x8a, 0xa9, 0x77, 0x6e, 0xd8, 0x4e, 0xa4, 0x9f, 0x07, + 0x23, 0xd6, 0x60, 0xc7, 0xa3, 0x86, 0x1f, 0x24, 0x6b, 0x3a, 0x79, 0xec, 0x72, 0x9f, 0x33, + 0x4f, 0x8a, 0x10, 0x03, 0xdc, 0x56, 0x35, 0x4e, 0xef, 0x23, 0x05, 0xd7, 0x4b, 0x52, 0xde, + 0x1e, 0x26, 0x74, 0xaf, 0x4e, 0xd1, 0xdf, 0xd5, 0x75, 0x17, 0x18, 0x23, 0x4b, 0xd8, 0x67, + 0xd3, 0xc7, 0x3e, 0x57, 0xc7, 0xf1, 0xf7, 0xae, 0xa6, 0xf6, 0xa7, 0xa4, 0xb1, 0x87, 0xc0, + 0xde, 0x14, 0xa8, 0x5b, 0x64, 0x7c, 0x69, 0xe1, 0x3c, 0x81, 0xc1, 0x09, 0x23, 0xd3, 0x24, + 0xad, 0xbb, 0x71, 0xe5, 0xdf, 0x09, 0x93, 0x21, 0x4c, 0x0f, 0x7d, 0x0d, 0x1d, 0x4f, 0x54, + 0x27, 0xd3, 0xb9, 0x6e, 0xb6, 0x7d, 0x43, 0x11, 0x1c, 0x62, 0xd7, 0xa3, 0x2e, 0xcf, 0x8c, + 0x83, 0x4b, 0x2d, 0xe0, 0xe2, 0x65, 0x1f, 0x91, 0x71, 0x67, 0x6c, 0xab, 0xbb, 0x0b, 0x62, + 0x11, 0xe0, 0x43, 0xf6, 0xe6, 0xe7, 0xcc, 0xf9, 0x41, 0x85, 0xb5, 0xc1, 0x76, 0x55, 0xd1, + 0xe6, 0xd2, 0x87, 0xc5, 0x2a, 0xcf, 0x83, 0x90, 0x42, 0x5a, 0xe2, 0xf7, 0x9b, 0xca, 0x09, + 0x2c, 0x3f, 0x2a, 0xb6, 0x1d, 0x59, 0xce, 0xe0, 0x3b, 0x94, 0x9e, 0x81, 0x6d, 0xed, 0x08, + 0x88, 0xc4, 0xc2, 0xa2, 0xab, 0x47, 0x3b, 0x48, 0x1d, 0xea, 0xc1, 0xe0, 0x43, 0x09, 0x46, + 0xab, 0x31, 0x4e, 0x5e, 0x81, 0x09, 0x3b, 0x00, 0xda, 0xe0, 0xc4, 0x35, 0x58, 0x4e, 0xb8, + 0xf6, 0xc7, 0xda, 0x8c, 0xf2, 0xba, 0xb0, 0xb1, 0x04, 0xff, 0x46, 0xbb, 0x51, 0x84, 0xa3, + 0xbc, + ]; + + fn m_hash(message: &[u8]) -> [u8; 32] { + let mut m_hash = [0u8; 32]; + SHA256::hash_out(message, &mut m_hash); + m_hash + } + + fn encode(message: &[u8], salt: &[u8; 32]) -> [u8; 256] { + emsa_pss_encode_from_hash::(&m_hash(message), salt) + } + + fn verify(message: &[u8], em: &[u8; 256]) -> bool { + emsa_pss_verify_from_hash::(&m_hash(message), em) + } + + #[test] + fn encode_matches_python_kat() { + assert_eq!(encode(b"hello", &SALT), EXPECTED_EM); + } + + #[test] + fn verify_accepts_the_kat() { + assert!(verify(b"hello", &EXPECTED_EM)); + } + + #[test] + fn verify_rejects_wrong_message() { + assert!(!verify(b"goodbye", &EXPECTED_EM)); + } + + #[test] + fn different_salts_give_different_but_both_valid_encodings() { + let salt_b: [u8; 32] = { + let mut s = SALT; + s[0] ^= 0xff; + s + }; + let em_a = encode(b"hello", &SALT); + let em_b = encode(b"hello", &salt_b); + assert_ne!(em_a, em_b, "PSS is randomized: different salts must give different EM"); + assert!(verify(b"hello", &em_a)); + assert!(verify(b"hello", &em_b)); + } + + /// RFC 8017 SS9.1.2 step 4: the trailer byte must be `0xbc`. + #[test] + fn verify_rejects_wrong_trailer_byte() { + let mut em = EXPECTED_EM; + em[255] = 0xbd; + assert!(!verify(b"hello", &em)); + } + + /// RFC 8017 SS9.1.2 step 6: the leftmost bit of `maskedDB`'s first octet must already be 0 + /// (see the module docs on why this crate always needs exactly one bit clear). + #[test] + fn verify_rejects_top_bit_set() { + let mut em = EXPECTED_EM; + em[0] |= 0x80; + assert!(!verify(b"hello", &em)); + } + + /// RFC 8017 SS9.1.2 step 10: every byte of `PS` must be zero. + #[test] + fn verify_rejects_nonzero_padding() { + let mut em = EXPECTED_EM; + // Corrupt a byte inside the (unmasked) PS region by flipping the corresponding bit of the + // masked byte directly -- em[0] is PS's first (masked) byte after the top-bit clear. + em[0] ^= 0x40; + assert!(!verify(b"hello", &em)); + } + + /// RFC 8017 SS9.1.2 step 10: the octet right before the salt must be exactly `0x01`. + #[test] + fn verify_rejects_wrong_separator_byte() { + // The byte at DB position (DB_LEN - S_LEN - 1) = 223 - 32 - 1 = 190, masked. Flipping its + // least-significant bit changes the unmasked separator from 0x01 to 0x00. + let mut em = EXPECTED_EM; + em[190] ^= 0x01; + assert!(!verify(b"hello", &em)); + } +} diff --git a/crypto/rsa/src/emsa_pss_shake.rs b/crypto/rsa/src/emsa_pss_shake.rs new file mode 100644 index 000000000..d0056e34e --- /dev/null +++ b/crypto/rsa/src/emsa_pss_shake.rs @@ -0,0 +1,249 @@ +//! EMSA-PSS with a SHAKE hash and native-SHAKE mask generation (RFC 8702 §3.2.1), rather than +//! MGF1: RFC 8017's own EMSA-PSS-ENCODE/VERIFY (§9.1), but per RFC 8702 §3.2.1 "the SHAKEs MUST be +//! used natively as the MGF, instead of the MGF1 algorithm ... In other words, the MGF is defined +//! as the SHAKE128 or SHAKE256 with input being the mgfSeed". A SHAKE (`X: XOF`) can already +//! produce an arbitrary-length output directly from one absorb, so this module has no counterpart +//! to [`crate::mgf1`] at all -- the "mask generation function" step is just `X::default().xof_out` +//! on the seed, one call instead of MGF1's counter loop. +//! +//! Otherwise this is [`crate::emsa_pss`] with `X` standing in for the hash: same steps, same +//! `8 * K_LEN - emBits == 1` simplification (see that module's docs) for the same reason (every +//! modulus size here is a whole number of 64-bit limbs). RFC 8702 §3.2.1 also fixes the hash +//! output length `H_LEN` and salt length `S_LEN` to be equal (32 bytes for SHAKE128, 64 for +//! SHAKE256) -- already this crate's own "typical" salt-length convention for MGF1-based PSS, so +//! no new relationship needs stating. + +use bouncycastle_core::traits::{HashOneShot, XOF}; + +/// EMSA-PSS-ENCODE (RFC 8017 §9.1.1) from step 3 onward with the mask generation function +/// replaced by native SHAKE output (RFC 8702 §3.2.1), given steps 1-2's `mHash = Hash(M)` by the +/// caller exactly as [`crate::emsa_pss::emsa_pss_encode_from_hash`] is. See the module docs for +/// `emBits` and the salt argument. +pub fn emsa_pss_encode_shake_from_hash< + X: XOF + HashOneShot, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const K_LEN: usize, +>( + m_hash: &[u8; H_LEN], + salt: &[u8; S_LEN], +) -> [u8; K_LEN] { + debug_assert_eq!(M_PRIME_LEN, 8 + H_LEN + S_LEN, "emsa_pss_encode_shake: M_PRIME_LEN mismatch"); + debug_assert_eq!(DB_LEN, K_LEN - H_LEN - 1, "emsa_pss_encode_shake: DB_LEN mismatch"); + debug_assert!( + K_LEN >= H_LEN + S_LEN + 2, + "emsa_pss_encode_shake: modulus too short for this hash/salt" + ); + + // Steps 5-6: M' = 8 zero octets || mHash || salt; H = Hash(M'). + let mut m_prime = [0u8; M_PRIME_LEN]; + m_prime[8..8 + H_LEN].copy_from_slice(m_hash); + m_prime[8 + H_LEN..].copy_from_slice(salt); + let mut h = [0u8; H_LEN]; + X::hash_out(&m_prime, &mut h); + + // Steps 7-8: DB = PS || 0x01 || salt (PS is all-zero, already the array's initial value). + let mut db = [0u8; DB_LEN]; + db[DB_LEN - S_LEN - 1] = 0x01; + db[DB_LEN - S_LEN..].copy_from_slice(salt); + + // Steps 9-11: maskedDB = DB xor MGF(H, DB_LEN), the SHAKE itself in place of MGF1 (RFC 8702 + // SS3.2.1), then clear the top bit (see module docs). + let mut db_mask = [0u8; DB_LEN]; + X::default().xof_out(&h, &mut db_mask); + let mut masked_db = [0u8; DB_LEN]; + for i in 0..DB_LEN { + masked_db[i] = db[i] ^ db_mask[i]; + } + masked_db[0] &= 0x7f; + + // Step 12: EM = maskedDB || H || 0xbc. + let mut em = [0u8; K_LEN]; + em[..DB_LEN].copy_from_slice(&masked_db); + em[DB_LEN..DB_LEN + H_LEN].copy_from_slice(&h); + em[K_LEN - 1] = 0xbc; + em +} + +/// EMSA-PSS-VERIFY (RFC 8017 §9.1.2) from step 4 onward with the mask generation function +/// replaced by native SHAKE output, as [`emsa_pss_encode_shake_from_hash`], and step 2's +/// `mHash = Hash(M)` supplied by the caller the same way. +pub fn emsa_pss_verify_shake_from_hash< + X: XOF + HashOneShot, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const K_LEN: usize, +>( + m_hash: &[u8; H_LEN], + em: &[u8; K_LEN], +) -> bool { + // Step 4. + if em[K_LEN - 1] != 0xbc { + return false; + } + // Step 6 (checked directly on `em[0]`, which is maskedDB's own leftmost octet). + if em[0] & 0x80 != 0 { + return false; + } + + // Step 5. + let mut masked_db = [0u8; DB_LEN]; + masked_db.copy_from_slice(&em[..DB_LEN]); + let mut h = [0u8; H_LEN]; + h.copy_from_slice(&em[DB_LEN..DB_LEN + H_LEN]); + + // Steps 7-9. + let mut db_mask = [0u8; DB_LEN]; + X::default().xof_out(&h, &mut db_mask); + let mut db = [0u8; DB_LEN]; + for i in 0..DB_LEN { + db[i] = masked_db[i] ^ db_mask[i]; + } + db[0] &= 0x7f; + + // Step 10. + let ps_len = DB_LEN - S_LEN - 1; + if db[..ps_len].iter().any(|&b| b != 0) || db[ps_len] != 0x01 { + return false; + } + + // Step 11. + let mut salt = [0u8; S_LEN]; + salt.copy_from_slice(&db[DB_LEN - S_LEN..]); + + // Steps 12-14 (step 2's mHash is the caller's `m_hash`). + let mut m_prime = [0u8; M_PRIME_LEN]; + m_prime[8..8 + H_LEN].copy_from_slice(m_hash); + m_prime[8 + H_LEN..].copy_from_slice(&salt); + let mut h_prime = [0u8; H_LEN]; + X::hash_out(&m_prime, &mut h_prime); + + h == h_prime +} + +#[cfg(test)] +mod tests { + //! `emsa_pss_encode_shake_from_hash`/`emsa_pss_verify_shake_from_hash` are crate-private + //! (only [`crate::rsassa_pss_shake`] needs them), so they are exercised here rather than from + //! `tests/` -- the same "high-risk code that cannot be reached through the public API" + //! exception `rsa_core`'s tests use. The KAT is a direct Python transliteration of RFC 8017 + //! SS9.1.1's own steps with RFC 8702 SS3.2.1's native-SHAKE mask substituted for MGF1 (not from + //! recall), using `hashlib.shake_128`. The `encode`/`verify` helpers below add step 2's + //! `mHash = Hash(M)` back on top, so the tests can speak in terms of messages. + + use super::*; + + use bouncycastle_sha3::SHAKE128; + + const SALT: [u8; 32] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, + ]; + + fn m_hash(message: &[u8]) -> [u8; 32] { + let mut m_hash = [0u8; 32]; + SHAKE128::hash_out(message, &mut m_hash); + m_hash + } + + fn encode(message: &[u8], salt: &[u8; 32]) -> [u8; 256] { + emsa_pss_encode_shake_from_hash::(&m_hash(message), salt) + } + + fn verify(message: &[u8], em: &[u8; 256]) -> bool { + emsa_pss_verify_shake_from_hash::(&m_hash(message), em) + } + + #[test] + fn encode_matches_python_kat() { + // hashlib.shake_128, following EMSA-PSS-ENCODE (RFC 8017 SS9.1.1) with the SHAKE-native + // mask of RFC 8702 SS3.2.1 substituted for MGF1 -- not from recall. + let expected_em: [u8; 256] = [ + 0x44, 0x7f, 0x52, 0xb3, 0xea, 0xb3, 0x57, 0x7d, 0xe7, 0x42, 0xb3, 0x95, 0xf6, 0x85, + 0xc1, 0x72, 0x43, 0xf1, 0xe1, 0x00, 0xca, 0x98, 0x51, 0x95, 0x9b, 0x49, 0x94, 0x8c, + 0x34, 0xe8, 0x80, 0x43, 0xb3, 0x7a, 0xe6, 0xf9, 0x58, 0x55, 0x1c, 0x24, 0x67, 0x2c, + 0x5c, 0x67, 0x89, 0xd6, 0x70, 0xd9, 0x01, 0x5c, 0x21, 0x06, 0xdf, 0xdf, 0x2c, 0x99, + 0x36, 0x25, 0xb7, 0x42, 0x0e, 0xcb, 0x9d, 0x06, 0xac, 0xe7, 0x28, 0x81, 0x01, 0xe3, + 0x37, 0xb0, 0x20, 0x84, 0x01, 0x39, 0x2a, 0x9b, 0x5c, 0xf7, 0x05, 0xef, 0x4c, 0xb2, + 0x41, 0xc6, 0x33, 0x88, 0x2f, 0x45, 0x8a, 0x70, 0xfc, 0xe2, 0xb9, 0xd1, 0xcb, 0x0a, + 0xe7, 0x8d, 0x94, 0xfa, 0x99, 0xf6, 0x28, 0xbc, 0x25, 0x38, 0x11, 0xc9, 0xfa, 0xfd, + 0xd0, 0x3e, 0x51, 0x0f, 0xc9, 0xd1, 0x1f, 0x2a, 0x21, 0x08, 0x26, 0xe8, 0xb3, 0x5a, + 0xc7, 0x08, 0xbf, 0xfe, 0xd9, 0x88, 0xde, 0xad, 0x26, 0xec, 0x86, 0x9c, 0x76, 0xb9, + 0x0e, 0xca, 0x0d, 0xfe, 0xad, 0x99, 0x16, 0x57, 0x45, 0x7f, 0x9f, 0x94, 0x4b, 0x8e, + 0x3f, 0xfb, 0xff, 0x11, 0x95, 0xf7, 0x70, 0x1b, 0x2c, 0xbc, 0xb4, 0xbd, 0x6c, 0x2f, + 0xe7, 0x10, 0xad, 0x21, 0xaa, 0xfc, 0x72, 0xfa, 0xd1, 0x79, 0x98, 0xcc, 0x1c, 0xfc, + 0x7c, 0x57, 0xca, 0xf9, 0x28, 0x54, 0xd5, 0xbf, 0x9c, 0x19, 0x6e, 0xea, 0x48, 0x46, + 0xdb, 0x40, 0xcf, 0x69, 0x12, 0x19, 0xfe, 0xe9, 0x0c, 0x2b, 0x5e, 0xbc, 0xe1, 0x23, + 0x28, 0x6b, 0xe8, 0x99, 0x00, 0x35, 0x0a, 0x45, 0x48, 0x06, 0xcd, 0xe0, 0x56, 0xf8, + 0xe6, 0x24, 0x68, 0x9f, 0x87, 0x9b, 0x7a, 0xa5, 0x0e, 0x5c, 0x42, 0xd3, 0x29, 0xab, + 0xe0, 0xa3, 0x21, 0xc7, 0xe2, 0x11, 0xfb, 0x27, 0x45, 0xbc, 0x20, 0x21, 0x41, 0xa6, + 0xd6, 0xc1, 0xd0, 0xbc, + ]; + assert_eq!(encode(b"hello", &SALT), expected_em); + } + + #[test] + fn round_trip_own_encoding() { + let em = encode(b"hello", &SALT); + assert!(verify(b"hello", &em)); + } + + #[test] + fn verify_rejects_wrong_message() { + let em = encode(b"hello", &SALT); + assert!(!verify(b"goodbye", &em)); + } + + #[test] + fn different_salts_give_different_but_both_valid_encodings() { + let salt_b: [u8; 32] = { + let mut s = SALT; + s[0] ^= 0xff; + s + }; + let em_a = encode(b"hello", &SALT); + let em_b = encode(b"hello", &salt_b); + assert_ne!(em_a, em_b, "PSS is randomized: different salts must give different EM"); + assert!(verify(b"hello", &em_a)); + assert!(verify(b"hello", &em_b)); + } + + /// RFC 8017 §9.1.2 step 4: the trailer byte must be `0xbc`. + #[test] + fn verify_rejects_wrong_trailer_byte() { + let mut em = encode(b"hello", &SALT); + em[255] = 0xbd; + assert!(!verify(b"hello", &em)); + } + + /// RFC 8017 §9.1.2 step 6: the leftmost bit of `maskedDB`'s first octet must already be 0. + #[test] + fn verify_rejects_top_bit_set() { + let mut em = encode(b"hello", &SALT); + em[0] |= 0x80; + assert!(!verify(b"hello", &em)); + } + + /// RFC 8017 §9.1.2 step 10: every byte of `PS` must be zero. + #[test] + fn verify_rejects_nonzero_padding() { + let mut em = encode(b"hello", &SALT); + em[0] ^= 0x40; + assert!(!verify(b"hello", &em)); + } + + /// RFC 8017 §9.1.2 step 10: the octet right before the salt must be exactly `0x01`. + #[test] + fn verify_rejects_wrong_separator_byte() { + // The byte at DB position (DB_LEN - S_LEN - 1) = 223 - 32 - 1 = 190, masked. Flipping its + // least-significant bit changes the unmasked separator from 0x01 to 0x00. + let mut em = encode(b"hello", &SALT); + em[190] ^= 0x01; + assert!(!verify(b"hello", &em)); + } +} diff --git a/crypto/rsa/src/keygen.rs b/crypto/rsa/src/keygen.rs new file mode 100644 index 000000000..20cd5aa13 --- /dev/null +++ b/crypto/rsa/src/keygen.rs @@ -0,0 +1,578 @@ +//! RSA key pair generation: FIPS 186-5 Appendix A.1.3, "Generation of Random Primes that are +//! Probably Prime", with Appendix B.3.1's Miller-Rabin test (behind Appendix B.7-style trial +//! division) and Appendix A.1.1's criteria on `e`, `p`, `q` and `d`, over this crate's fixed-width +//! limb arithmetic -- there is no arbitrary-precision integer type here, as in the rest of the +//! crate. Structured after BC Java's `org.bouncycastle.crypto.generators.RSAKeyPairGenerator` +//! (its candidate loop, small-factor sieve and `d` lower-bound check are what this follows), with +//! FIPS 186-5 as the reference for every step and cited inline. +//! +//! The concrete sizes wire this up as `keygen(rng)` in their own modules +//! (`crate::rsa_2048::keygen` and siblings): `nlen` is fixed by the width, `e` by +//! [`PUBLIC_EXPONENT`], and the Miller-Rabin round count and required RNG strength by each size's +//! row of FIPS 186-5 Table B.1 and SP 800-57 Part 1 Table 2. +//! +//! # What is and is not constant-time here +//! +//! Key generation is rejection sampling: how many candidates are drawn, and where each one is +//! rejected (a small factor, the `|p - q|` bound, a Miller-Rabin witness), is inherently +//! data-dependent, and the small-modulus arithmetic on a candidate -- its residues modulo the +//! trial-division primes and modulo `e`, the binary GCD and the one long division behind +//! `lcm(p - 1, q - 1)` -- uses ordinary variable-time integer operations. That is the same +//! position BC Java's `BigInteger`-based generator takes, for a one-off operation with no +//! attacker-supplied input. The secret-exponent work generation does do, `qInv = q^(p - 2) mod p`, +//! goes through [`crate::modexp::mod_pow`]'s constant-time ladder, the same one every signature +//! uses; and the `MontgomeryContext` built over each candidate is the same construction +//! `rsa_core::rsasp1` already builds over the secret primes on every signature. + +use crate::keys::{RsaPrivateKey, RsaPublicKey}; +use crate::modexp::{MontgomeryContext, mod_pow, mul_mod, reduce_wide}; +use bouncycastle_core::errors::{RNGError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::RNG; +use bouncycastle_ec::montgomery; +use bouncycastle_ec::nat; +use bouncycastle_utils::secret::Secret; +use core::num::NonZeroUsize; + +/// The public exponent every key pair this crate generates uses: `F4 = 2^16 + 1 = 65537`. +/// FIPS 186-5 Appendix A.1.1 criterion 1 asks for an odd `e` with `2^16 < e < 2^256`, "selected +/// prior to generating the primes"; this is the smallest such value and the near-universal choice +/// (it is what Wycheproof's and BC Java's test keys use). Its primality is what lets criterion +/// 2(a), `GCD(p - 1, e) = 1`, be checked as "`e` does not divide `p - 1`" (step 4.5 below). +pub const PUBLIC_EXPONENT: u32 = 65537; + +/// Trial-division bound: every prime below this is divided out of a candidate before the +/// Miller-Rabin test runs (FIPS 186-5 Appendix B.7, "if convenient, `c` may be divided by +/// composite numbers" -- the primes are bundled into `u64` products, as BC Java's +/// `Primes.hasAnySmallFactors` does). `2^10` is a round choice well inside Appendix B.8's +/// "typical" `10^3` to `10^5`; about 93% of odd composites are caught here, sparing that many +/// modular exponentiations. +pub const SMALL_PRIME_LIMIT: usize = 1024; + +/// `pi(1024)`: the number of primes below [`SMALL_PRIME_LIMIT`]. +const SMALL_PRIME_COUNT: usize = 172; + +/// The primes below [`SMALL_PRIME_LIMIT`], from a compile-time sieve of Eratosthenes (Appendix +/// B.8's sieve procedure, at its simplest). +const SMALL_PRIMES: [u16; SMALL_PRIME_COUNT] = small_primes(); + +const fn small_primes() -> [u16; SMALL_PRIME_COUNT] { + let mut composite = [false; SMALL_PRIME_LIMIT]; + let mut primes = [0u16; SMALL_PRIME_COUNT]; + let mut found = 0; + let mut i = 2; + while i < SMALL_PRIME_LIMIT { + if !composite[i] { + primes[found] = i as u16; + found += 1; + let mut j = i * i; + while j < SMALL_PRIME_LIMIT { + composite[j] = true; + j += i; + } + } + i += 1; + } + assert!(found == SMALL_PRIME_COUNT, "SMALL_PRIME_COUNT must be pi(SMALL_PRIME_LIMIT)"); + primes +} + +// ---- small-integer helpers over `[u64; N]` little-endian limbs ------------------------------ + +/// `a mod m` for a small modulus `m`, by schoolbook long division one limb at a time. +fn mod_u64(a: &[u64; N], m: u64) -> u64 { + let mut rem: u128 = 0; + for limb in a.iter().rev() { + rem = ((rem << 64) | u128::from(*limb)) % u128::from(m); + } + rem as u64 +} + +/// `a * k + add`, as `N` limbs plus the carry limb. +fn mul_u64_add(a: &[u64; N], k: u64, add: u64) -> ([u64; N], u64) { + let mut out = [0u64; N]; + let mut carry = u128::from(add); + for i in 0..N { + let t = u128::from(a[i]) * u128::from(k) + carry; + out[i] = t as u64; + carry = t >> 64; + } + (out, carry as u64) +} + +/// `(hi || a) / d` and the remainder, for `hi < d`, by schoolbook long division one limb at a +/// time; the quotient then fits in `N` limbs. +fn div_u64(hi: u64, a: &[u64; N], d: u64) -> ([u64; N], u64) { + debug_assert!(hi < d, "div_u64: the quotient must fit in N limbs"); + let mut quotient = [0u64; N]; + let mut rem = u128::from(hi); + for i in (0..N).rev() { + let cur = (rem << 64) | u128::from(a[i]); + quotient[i] = (cur / u128::from(d)) as u64; + rem = cur % u128::from(d); + } + (quotient, rem as u64) +} + +/// `r^-1 mod e` for prime `e` and `0 < r < e`, as `r^(e - 2) mod e` (Fermat), by a fixed 32-step +/// square-and-multiply. +fn inv_mod_u64_prime(r: u64, e: u64) -> u64 { + debug_assert!(r != 0 && r < e); + let exp = e - 2; + let mut acc: u128 = 1; + let base = u128::from(r); + let m = u128::from(e); + for bit in (0..32).rev() { + acc = (acc * acc) % m; + if (exp >> bit) & 1 == 1 { + acc = (acc * base) % m; + } + } + acc as u64 +} + +/// `e^-1 mod m` for `m > 1` with `GCD(m, e) = 1`, where `e` is [`PUBLIC_EXPONENT`]. +/// +/// FIPS 186-5 Appendix B.1 gives the extended Euclidean algorithm for reference and allows +/// "other algorithms that produce an equivalent result". Because `e` is small and prime, the +/// inverse comes without a multi-limb division: with `r = m mod e` and `k = -r^-1 mod e` (so +/// `k*m + 1 ≡ 0 mod e`), `d = (k*m + 1) / e` is an exact integer with `e*d = 1 + k*m ≡ 1 mod m`, +/// and `0 < d < m` since `0 < k < e`. The only multi-limb operations are one multiply by `k` and +/// one exact division by `e`. +fn inverse_of_e_mod(m: &[u64; N]) -> [u64; N] { + let e = u64::from(PUBLIC_EXPONENT); + let r = mod_u64(m, e); + // `GCD(m, e) = 1` is the caller's invariant: `keygen` discards any `p` (or `q`) with + // `e | p - 1` at step 4.5, and `LCM(p - 1, q - 1)` then cannot be divisible by `e` either. + debug_assert_ne!(r, 0, "e must not divide m"); + let k = e - inv_mod_u64_prime(r, e); + let (lo, hi) = mul_u64_add(m, k, 1); + let (d, rem) = div_u64(hi, &lo, e); + debug_assert_eq!(rem, 0, "k*m + 1 is divisible by e by construction"); + d +} + +// ---- multi-limb helpers ---------------------------------------------------------------------- + +fn is_zero(a: &[u64; N]) -> bool { + nat::is_zero(a).to_bool() +} + +/// `a < b`. +fn lt(a: &[u64; N], b: &[u64; N]) -> bool { + nat::sub(a, b).1 == 1 +} + +fn trailing_zeros(a: &[u64; N]) -> u32 { + let mut count = 0; + for limb in a { + if *limb == 0 { + count += 64; + } else { + return count + limb.trailing_zeros(); + } + } + count +} + +fn leading_zeros(a: &[u64; N]) -> u32 { + let mut count = 0; + for limb in a.iter().rev() { + if *limb == 0 { + count += 64; + } else { + return count + limb.leading_zeros(); + } + } + count +} + +/// Clears every bit of `a` at position `bits` or above. +fn mask_to_bits(a: &mut [u64; N], bits: u32) { + for (i, limb) in a.iter_mut().enumerate() { + let lo = 64 * i as u32; + if bits <= lo { + *limb = 0; + } else if bits < lo + 64 { + *limb &= (1u64 << (bits - lo)) - 1; + } + } +} + +fn shr(a: &[u64; N], shift: u32) -> [u64; N] { + let mut out = [0u64; N]; + let limbs = (shift / 64) as usize; + let bits = shift % 64; + for i in 0..N.saturating_sub(limbs) { + let lo = a[i + limbs] >> bits; + let hi = if bits != 0 && i + limbs + 1 < N { a[i + limbs + 1] << (64 - bits) } else { 0 }; + out[i] = lo | hi; + } + out +} + +fn shl(a: &[u64; N], shift: u32) -> [u64; N] { + let mut out = [0u64; N]; + let limbs = (shift / 64) as usize; + let bits = shift % 64; + for i in limbs..N { + let lo = a[i - limbs] << bits; + let hi = if bits != 0 && i > limbs { a[i - limbs - 1] >> (64 - bits) } else { 0 }; + out[i] = lo | hi; + } + out +} + +/// `a - k` for a small `k <= a`. +fn sub_u64(a: &[u64; N], k: u64) -> [u64; N] { + let mut k_limbs = [0u64; N]; + k_limbs[0] = k; + let (diff, borrow) = nat::sub(a, &k_limbs); + debug_assert_eq!(borrow, 0); + diff +} + +/// `GCD(a, b)` by Stein's binary algorithm; variable time (see the module docs). +fn gcd(a: &[u64; N], b: &[u64; N]) -> [u64; N] { + if is_zero(a) { + return *b; + } + if is_zero(b) { + return *a; + } + let common_twos = trailing_zeros(a).min(trailing_zeros(b)); + let mut a = shr(a, trailing_zeros(a)); + let mut b = shr(b, trailing_zeros(b)); + loop { + // Both odd here, so `a - b` is even (or zero); dividing its powers of two out keeps the + // invariant, and the larger operand strictly shrinks each round. + if lt(&a, &b) { + core::mem::swap(&mut a, &mut b); + } + let (diff, _) = nat::sub(&a, &b); + if is_zero(&diff) { + break; + } + a = shr(&diff, trailing_zeros(&diff)); + } + shl(&b, common_twos) +} + +/// `a / d` for `d != 0`, by bit-serial schoolbook long division; variable time (see the module +/// docs). Used once per key pair, for `(q - 1) / GCD(p - 1, q - 1)`. +fn div(a: &[u64; N], d: &[u64; N]) -> [u64; N] { + debug_assert!(!is_zero(d)); + let mut rem = [0u64; N]; + let mut quotient = [0u64; N]; + for limb in (0..N).rev() { + for bit in (0..64).rev() { + // rem = 2*rem + next bit; rem < d before, so the true value is < 2d and one + // conditional subtraction restores rem < d. The bit that leaves the top limb is the + // "carry" case, in which the subtraction is always due. + let carry = rem[N - 1] >> 63; + rem = shl(&rem, 1); + rem[0] |= (a[limb] >> bit) & 1; + let (diff, borrow) = nat::sub(&rem, d); + if carry == 1 || borrow == 0 { + rem = diff; + quotient[limb] |= 1 << bit; + } + } + } + quotient +} + +/// Fills `out` with `64 * N` bits from `rng` (FIPS 186-5 Appendix B.2.1: the string is read as a +/// big-endian integer; here limb `N - 1` holds its most significant 64 bits). +fn fill_random( + rng: &mut dyn RNG, + out: &mut [u64; N], +) -> Result<(), SignatureError> { + let mut buf = [0u8; 8]; + for limb in out.iter_mut() { + rng.next_bytes_out(&mut buf).map_err(SignatureError::RNGError)?; + *limb = u64::from_be_bytes(buf); + } + Ok(()) +} + +// ---- primality ------------------------------------------------------------------------------- + +/// Whether some prime below [`SMALL_PRIME_LIMIT`] divides `w` -- other than `w` itself, so a small +/// prime is not reported as its own factor. The primes are bundled into `u64` products and each +/// bundle costs one multi-limb reduction (Appendix B.7's "may be divided by composite numbers"). +fn has_small_factor(w: &[u64; N]) -> bool { + let fits_u64 = w[1..].iter().all(|&l| l == 0); + let mut i = 0; + while i < SMALL_PRIME_COUNT { + let start = i; + let mut product: u64 = 1; + while i < SMALL_PRIME_COUNT { + match product.checked_mul(u64::from(SMALL_PRIMES[i])) { + Some(next) => { + product = next; + i += 1; + } + None => break, + } + } + let r = mod_u64(w, product); + for &p in &SMALL_PRIMES[start..i] { + let p = u64::from(p); + if r % p == 0 && !(fits_u64 && w[0] == p) { + return true; + } + } + } + false +} + +/// FIPS 186-5 Appendix B.3.1, the Miller-Rabin probabilistic primality test, with `rounds` +/// iterations whose bases come from `rng`: `Ok(true)` is PROBABLY PRIME, `Ok(false)` is COMPOSITE. +/// Preceded by trial division against the primes below [`SMALL_PRIME_LIMIT`] (Appendix B.7), +/// which is conclusive on its own for `w < 2^20` and otherwise only ever rejects. +/// +/// `rounds` must be at least the value FIPS 186-5 Table B.1 gives for the candidate's length and +/// the caller's target error probability (see each size module's `keygen` for the value it uses); +/// its type rules out zero rounds at compile time. Bases that fall outside `[2, w - 2]` are +/// redrawn (step 4.2); an RNG that cannot produce one in 128 draws is reported as +/// `Err(GenericError)`, so a broken RNG cannot hang the test. +pub fn is_probable_prime( + w: &[u64; N], + rounds: NonZeroUsize, + rng: &mut dyn RNG, +) -> Result { + let mut one = [0u64; N]; + one[0] = 1; + let mut two = [0u64; N]; + two[0] = 2; + if is_zero(w) || *w == one { + return Ok(false); + } + if w[0] & 1 == 0 { + return Ok(*w == two); + } + let fits_u64 = w[1..].iter().all(|&l| l == 0); + if fits_u64 && w[0] < (SMALL_PRIME_LIMIT * SMALL_PRIME_LIMIT) as u64 { + // Appendix B.7: no prime factor up to sqrt(w) means prime. + return Ok(!has_small_factor(w)); + } + if has_small_factor(w) { + return Ok(false); + } + + // Steps 1-2: w - 1 = 2^a * m with m odd. + let w_minus_1 = sub_u64(w, 1); + let a = trailing_zeros(&w_minus_1); + let m = shr(&w_minus_1, a); + // w is odd and nonzero here (both checked above), which is all `MontgomeryContext::new` + // requires, so this cannot fail. + let ctx = MontgomeryContext::new(w).expect("w is odd and nonzero"); + + // Step 3: wlen = len(w), the bit length of w -- the bases are drawn at that length (not the + // full limb width), so that for a w well short of the width a draw still lands below w - 1 + // with probability at least 1/2. + let wlen = 64 * N as u32 - leading_zeros(w); + + // Step 4. + for _ in 0..rounds.get() { + // Steps 4.1-4.2: a random base b of wlen bits, redrawn unless in [2, w - 2]. + let mut b = [0u64; N]; + let mut drawn = false; + for _ in 0..128 { + fill_random(rng, &mut b)?; + mask_to_bits(&mut b, wlen); + if lt(&one, &b) && lt(&b, &w_minus_1) { + drawn = true; + break; + } + } + if !drawn { + return Err(SignatureError::GenericError( + "RNG produced no usable Miller-Rabin base in 128 draws", + )); + } + // Step 4.3. + let mut z = mod_pow::(&b, &m, &ctx); + // Step 4.4. + if z == one || z == w_minus_1 { + continue; + } + // Step 4.5. + let mut witness = true; + for _ in 1..a { + z = mul_mod::(&z, &z, &ctx); + if z == w_minus_1 { + witness = false; + break; + } + if z == one { + break; + } + } + if witness { + // Step 4.6. + return Ok(false); + } + } + // Step 5. + Ok(true) +} + +// ---- key pair generation --------------------------------------------------------------------- + +/// A candidate for `p` or `q` (FIPS 186-5 Appendix A.1.3 steps 4.2-4.3 / 5.2-5.3): `64 * N` +/// random bits with the two most significant bits set (step 4.2.1's option) and forced odd (step +/// 4.3, "if p is not odd, then p = p + 1"). Setting both top bits makes step 4.4's lower bound +/// `p >= sqrt(2) * 2^(nlen/2 - 1)` hold by construction -- `0.75 * 2^(nlen/2) > 0.7071 * +/// 2^(nlen/2)` -- and makes `n = p * q` exactly `nlen` bits, so the bound is only +/// `debug_assert`ed rather than tested and retried. +fn candidate(rng: &mut dyn RNG, out: &mut [u64; N]) -> Result<(), SignatureError> { + fill_random(rng, out)?; + out[N - 1] |= 0b11 << 62; + out[0] |= 1; + // sqrt(2) * 2^63 = 0xB504F333F9DE6484...: with the top two bits set the top limb is at least + // 0xC000..., so this cannot fail. + debug_assert!(out[N - 1] > 0xB504_F333_F9DE_6484, "step 4.4's lower bound"); + Ok(()) +} + +/// FIPS 186-5 Appendix A.1.3 step 4 or 5: repeatedly draws a candidate until one passes +/// `GCD(p - 1, e) = 1` (step 4.5, as `e` not dividing `p - 1`, since `e` is prime), trial +/// division, and `rounds` of Miller-Rabin (step 4.5.1), giving up after `max_attempts` candidates +/// (step 4.7 / 5.8). `extra_check` is step 5.5's `|p - q|` test for `q`, and nothing for `p`. +fn random_prime( + rng: &mut dyn RNG, + rounds: NonZeroUsize, + max_attempts: usize, + out: &mut [u64; N], + extra_check: &dyn Fn(&[u64; N]) -> bool, +) -> Result<(), SignatureError> { + for _ in 0..max_attempts { + candidate(rng, out)?; + if !extra_check(out) { + continue; + } + // Step 4.5: GCD(p - 1, e) = 1, i.e. p mod e != 1 for prime e. + if mod_u64(out, u64::from(PUBLIC_EXPONENT)) == 1 { + continue; + } + if is_probable_prime::(out, rounds, rng)? { + return Ok(()); + } + } + // Step 4.7 / 5.8: FAILURE. Zero the last candidate before reporting, as the spec's zero + // outputs do. + *out = [0u64; N]; + Err(SignatureError::GenericError( + "RSA key generation exhausted FIPS 186-5 A.1.3's candidate limit without finding a prime", + )) +} + +/// Step 5.5: `|p - q| <= 2^(nlen/2 - 100)` rejects `q` (Appendix A.1.1 criterion 2(d) asks for +/// `|p - q| > 2^(nlen/2 - 100)`). `nlen/2 = 64 * N` here. +fn primes_too_close(p: &[u64; N], q: &[u64; N]) -> bool { + let (d1, borrow) = nat::sub(q, p); + let (d2, _) = nat::sub(p, q); + let diff = if borrow == 1 { d2 } else { d1 }; + let threshold_bit = 64 * N as u32 - 100; + let high = shr(&diff, threshold_bit); + if is_zero(&high) { + return true; // |p - q| < 2^(nlen/2 - 100) + } + let mut one = [0u64; N]; + one[0] = 1; + // Exactly 2^(nlen/2 - 100) is also "<=": the high part is 1 and nothing is set below it. + high == one && diff == shl(&one, threshold_bit) +} + +/// FIPS 186-5 Appendix A.1.3, the whole process, for a modulus of `HALF2` limbs (`nlen = 64 * +/// HALF2` bits) built from two `HALF`-limb primes. `rounds` is the Miller-Rabin iteration count +/// (Table B.1) and `required_strength` the security strength SP 800-57 Part 1 associates with +/// `nlen`, which the RNG must meet (step 3 and the paragraph above the process). The size modules +/// fix all three; see `crate::rsa_2048::keygen` for the RSA-2048 values. +/// +/// Returns the pair as this crate's key types: `(n, e)` and the CRT quintuple, with `dP = e^-1 mod +/// (p - 1)`, `dQ = e^-1 mod (q - 1)` (which equal `d mod (p - 1)` and `d mod (q - 1)` for the +/// `d = e^-1 mod LCM(p - 1, q - 1)` of Appendix A.1.1 criterion 3(b)) and `qInv = q^-1 mod p`. +/// Criterion 3(a), `d > 2^(nlen/2)`, is checked on that `d` and the whole process restarts if it +/// fails -- "the extremely rare event" the spec describes, of probability about `2^-(nlen/2)`. +/// +/// Errors: `RNGError(SecurityStrengthInsufficientForAlgorithm)` if `rng` is weaker than +/// `required_strength`; `RNGError` for an RNG failure; `GenericError` if the candidate limits of +/// steps 4.7/5.8 (`5 * nlen` for `p`, `10 * nlen` for `q`) are exhausted, which a working RNG +/// does not do. +pub fn keygen_from_rng( + rng: &mut dyn RNG, + rounds: NonZeroUsize, + required_strength: SecurityStrength, +) -> Result<(RsaPublicKey, RsaPrivateKey), SignatureError> { + debug_assert_eq!(HALF2, 2 * HALF); + // Step 3 (and A.1.3's preamble): the DRBG's strength must meet the modulus's. + if rng.security_strength() < required_strength { + return Err(SignatureError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)); + } + let nlen = 64 * HALF2; + + loop { + // Held in `Secret` so the primes and everything derived from them are scrubbed when this + // function returns (or restarts), not left on the stack. + let mut p = Secret::<[u64; HALF]>::new(); + let mut q = Secret::<[u64; HALF]>::new(); + + // Step 4: generate p (up to 5 * nlen candidates). + random_prime::(rng, rounds, 5 * nlen, &mut p, &|_| true)?; + // Step 5: generate q (up to 10 * nlen candidates), rejecting one too close to p (5.5). + { + let p_ref: &[u64; HALF] = &p; + random_prime::(rng, rounds, 10 * nlen, &mut q, &|cand| { + !primes_too_close(p_ref, cand) + })?; + } + + // Appendix A.1.1 criterion 3: d = e^-1 mod LCM(p - 1, q - 1) must exceed 2^(nlen/2). + let mut p_minus_1 = Secret::<[u64; HALF]>::new(); + *p_minus_1 = sub_u64(&p, 1); + let mut q_minus_1 = Secret::<[u64; HALF]>::new(); + *q_minus_1 = sub_u64(&q, 1); + let mut lcm = Secret::<[u64; HALF2]>::new(); + { + let g = gcd(&p_minus_1, &q_minus_1); + let q_minus_1_over_g = div(&q_minus_1, &g); + *lcm = montgomery::widening_mul::(&p_minus_1, &q_minus_1_over_g); + } + let mut d = Secret::<[u64; HALF2]>::new(); + *d = inverse_of_e_mod(&lcm); + let d_high = &d[HALF..]; + let d_low = &d[..HALF]; + let mut one_high = [0u64; HALF]; + one_high[0] = 1; + let d_exceeds_half = d_high.iter().any(|&l| l != 0) + && !(d_high == one_high && d_low.iter().all(|&l| l == 0)); + if !d_exceeds_half { + // "then new values for p, q, and d shall be determined." + continue; + } + + // The CRT quintuple (RFC 8017 §3.2). dP and dQ are the same inverse taken modulo p - 1 + // and q - 1 directly, which is what `d mod (p - 1)` and `d mod (q - 1)` come to. + let mut d_p = Secret::<[u64; HALF]>::new(); + *d_p = inverse_of_e_mod(&p_minus_1); + let mut d_q = Secret::<[u64; HALF]>::new(); + *d_q = inverse_of_e_mod(&q_minus_1); + // qInv = q^-1 mod p = q^(p - 2) mod p (Fermat; p is prime and does not divide q), through + // the constant-time ladder with p - 2 as the exponent. + let mut q_inv = Secret::<[u64; HALF]>::new(); + { + let q_mod_p = reduce_wide::(&q, &p); + let p_minus_2 = sub_u64(&p, 2); + // p is odd by construction (step 4.3), so the context exists. + let ctx_p = MontgomeryContext::new(&p).expect("p is odd"); + *q_inv = mod_pow::(&q_mod_p, &p_minus_2, &ctx_p); + } + + let sk = RsaPrivateKey::::from_crt_components(&p, &q, &d_p, &d_q, &q_inv)?; + let pk = RsaPublicKey::::new(sk.n(), PUBLIC_EXPONENT)?; + return Ok((pk, sk)); + } +} diff --git a/crypto/rsa/src/keys.rs b/crypto/rsa/src/keys.rs new file mode 100644 index 000000000..63a793596 --- /dev/null +++ b/crypto/rsa/src/keys.rs @@ -0,0 +1,318 @@ +//! RSA key types: [`RsaPublicKey`] (RFC 8017 §3.1's `(n, e)`) and [`RsaPrivateKey`] (§3.2's +//! "second representation", the CRT quintuple `p`, `q`, `dP`, `dQ`, `qInv`), generic over the +//! modulus's limb count `L` and each prime's limb count `HALF`. +//! +//! Keys come either from [`crate::keygen`] (FIPS 186-5 Appendix A.1.3, via each size module's +//! `keygen`) or from externally supplied key material (e.g. loaded from CAVP/wycheproof test +//! vectors, or a key produced by another implementation) through [`RsaPrivateKey::from_crt_components`]. +//! RFC 8017's first representation, the plain `(n, d)` pair, is not supported -- every real RSA +//! private key ships as the CRT form because CRT-based signing (RFC 8017 §5.2.1 step 2.b) is the +//! only signing path this crate implements. +//! +//! Each concrete modulus size's aliases of these types (`crate::rsa_2048::RSA2048PrivateKey` and +//! siblings) implement `bouncycastle_core`'s `SignaturePrivateKey`/`SignaturePublicKey` traits, +//! in that size's module, with the byte layouts described on each type below; the generic types +//! here only carry the layout's implementation (`encode_raw`/`from_bytes_raw`, crate-private), +//! since the traits' `SK_LEN`/`PK_LEN` are fixed per size and stable Rust cannot derive them from +//! `L`. + +use crate::codec::{be_bytes_from_limbs, limbs_from_be_bytes}; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_ec::montgomery; +use bouncycastle_ec::nat; +use bouncycastle_utils::secret::Secret; +use core::fmt; +use core::fmt::{Display, Formatter}; + +/// An RSA public key: RFC 8017 §3.1's `(n, e)`. +/// +/// # Encoding +/// +/// `SignaturePublicKey::encode`/`from_bytes` (implemented per size, e.g. for +/// `crate::rsa_2048::RSA2048PublicKey`) use the raw layout `n || e`: `n` as `8 * L` big-endian +/// bytes (RFC 8017 §4.1's I2OSP), then `e` as 4 big-endian bytes, `PK_LEN = 8 * L + 4` in all. +/// Not an RFC 8017 or ASN.1 format -- this crate has no DER encoder/decoder -- just a fixed-width +/// layout that round-trips, for `cli/src/rsa_cmd.rs` to read and write public keys as files. +/// Decoding validates `(n, e)` the same way [`Self::new`] does. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RsaPublicKey { + n: [u64; L], + e: u32, +} + +impl RsaPublicKey { + /// Builds a public key from its modulus and exponent. Returns + /// `Err(`[`SignatureError::DecodingError`]`)` if `n` is even, or if `e` is even or less than 3 + /// (RFC 8017 §3.1: "the RSA public exponent e is an integer between 3 and n - 1"; the upper + /// bound is not checked here -- it needs a full multi-limb comparison against `n` for a + /// boundary no key from a real implementation will ever hit). There is no separate all-zero + /// check on `n`: a zero value's low limb is `0`, which the oddness check already rejects. + pub fn new(n: &[u64; L], e: u32) -> Result { + if n[0] & 1 == 0 { + return Err(SignatureError::DecodingError("RSA modulus must be odd")); + } + if e < 3 || e & 1 == 0 { + return Err(SignatureError::DecodingError( + "RSA public exponent must be odd and at least 3", + )); + } + Ok(Self { n: *n, e }) + } + + /// The modulus `n`. + pub fn n(&self) -> &[u64; L] { + &self.n + } + + /// The public exponent `e`. + pub fn e(&self) -> u32 { + self.e + } + + /// The `# Encoding` layout (see the type's docs), `n || e`, with `N_BYTES = 8 * L` and + /// `PK_LEN = N_BYTES + 4` supplied by the per-size `SignaturePublicKey` impl that wraps this. + pub(crate) fn encode_raw(&self) -> [u8; PK_LEN] { + debug_assert_eq!(N_BYTES, 8 * L, "RsaPublicKey::encode_raw needs N_BYTES == 8 * L"); + debug_assert_eq!( + PK_LEN, + N_BYTES + 4, + "RsaPublicKey::encode_raw needs PK_LEN == N_BYTES + 4" + ); + let mut out = [0u8; PK_LEN]; + out[..N_BYTES].copy_from_slice(&be_bytes_from_limbs::(&self.n)); + out[N_BYTES..].copy_from_slice(&self.e.to_be_bytes()); + out + } + + /// Decodes [`Self::encode_raw`]'s layout, validated the same way [`Self::new`] validates + /// `(n, e)`. Takes the exact-length array; the per-size `SignaturePublicKey::from_bytes` that + /// wraps this is where a wrong-length slice is rejected. + pub(crate) fn from_bytes_raw( + bytes: &[u8; PK_LEN], + ) -> Result { + debug_assert_eq!(N_BYTES, 8 * L, "RsaPublicKey::from_bytes_raw needs N_BYTES == 8 * L"); + debug_assert_eq!( + PK_LEN, + N_BYTES + 4, + "RsaPublicKey::from_bytes_raw needs PK_LEN == N_BYTES + 4" + ); + let n_bytes: [u8; N_BYTES] = bytes[..N_BYTES].try_into().expect("N_BYTES-byte slice"); + let n = limbs_from_be_bytes::(&n_bytes); + let e_bytes: [u8; 4] = bytes[N_BYTES..].try_into().expect("4-byte slice"); + Self::new(&n, u32::from_be_bytes(e_bytes)) + } +} + +/// `RsaPublicKey { n: , e: }`, `n` as one big-endian hex string (the limbs are +/// stored least-significant first, so they are printed in reverse). Required by +/// `SignaturePublicKey`'s `Display` supertrait; public data only, so nothing is redacted. +impl Display for RsaPublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "RsaPublicKey<{L}> {{ n: ")?; + for limb in self.n.iter().rev() { + write!(f, "{limb:016x}")?; + } + write!(f, ", e: {:x} }}", self.e) + } +} + +/// An RSA private key in RFC 8017 §3.2's CRT ("second representation") form: `p`, `q`, `dP`, +/// `dQ`, `qInv`. `L` (the modulus's limb count) must be exactly `2 * HALF`; like +/// `bouncycastle_ec::montgomery`'s `L2 = 2 * L`, that relationship is enforced at construction +/// (see [`Self::from_crt_components`]) rather than expressed in the type, because stable Rust has +/// no way to compute one const generic from another. +/// +/// `p` and `q` need not be the same bit length: RFC 8017 §3.2 requires only that both are prime +/// and `p * q = n`, nothing about their relative magnitude. An earlier version of this type +/// required equal bit length anyway, so its CRT recombination could reduce one residue mod the +/// other prime with a single conditional subtraction (valid only when the smaller prime's residue +/// is `< 2 *` the larger prime, which equal bit length guarantees but does not itself require). +/// [`crate::modexp::reduce_wide`]'s general bit-serial reduction handles any magnitude +/// relationship instead, at the same asymptotic cost this crate already pays to reduce the +/// `n`-width message down to each prime's width, so the extra restriction bought nothing and is +/// not reimposed here. +/// +/// # Encoding +/// +/// `SignaturePrivateKey::encode`/`from_bytes` (implemented per size, e.g. for +/// `crate::rsa_2048::RSA2048PrivateKey`) use the raw layout `p || q || dP || dQ || qInv`, each +/// `8 * HALF` big-endian bytes, `SK_LEN = 40 * HALF` in all. Not an RFC 8017 or ASN.1 format -- +/// see [`RsaPublicKey`]'s `# Encoding` for why -- just a fixed-width layout that round-trips, for +/// `cli/src/rsa_cmd.rs` to read and write private keys as files. Decoding validates the five +/// components the same way [`Self::from_crt_components`] does. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RsaPrivateKey { + p: Secret<[u64; HALF]>, + q: Secret<[u64; HALF]>, + d_p: Secret<[u64; HALF]>, + d_q: Secret<[u64; HALF]>, + q_inv: Secret<[u64; HALF]>, + /// `n = p * q`, computed once here. Public: `n` is shared with the matching public key (RFC + /// 8017 §3.2, "the RSA modulus n is the same as in the corresponding RSA public key"). + n: [u64; L], +} + +impl RsaPrivateKey { + /// Builds a private key from its CRT components. Returns + /// `Err(`[`SignatureError::DecodingError`]`)` if: + /// + /// * `L != 2 * HALF` (a caller programming error -- every concrete instantiation in this + /// crate pairs a fixed `L` with a fixed `HALF`, so this cannot happen through the crate's + /// own public API, only through an explicit, wrong turbofish); + /// * `p` or `q` is even or equal to each other. There is no separate all-zero check: a zero + /// value's low limb is `0`, which the oddness check already rejects. + /// * `dP >= p`, `dQ >= q`, or `qInv >= p` (RFC 8017 §3.2: `dP`/`dQ`/`qInv` are each "a + /// positive integer less than" their modulus). + pub fn from_crt_components( + p: &[u64; HALF], + q: &[u64; HALF], + d_p: &[u64; HALF], + d_q: &[u64; HALF], + q_inv: &[u64; HALF], + ) -> Result { + if L != 2 * HALF { + return Err(SignatureError::DecodingError("RSA modulus width must be 2 * prime width")); + } + if p[0] & 1 == 0 { + return Err(SignatureError::DecodingError("p must be odd")); + } + if q[0] & 1 == 0 { + return Err(SignatureError::DecodingError("q must be odd")); + } + if p == q { + return Err(SignatureError::DecodingError("p and q must be distinct")); + } + if nat::sub(d_p, p).1 != 1 { + return Err(SignatureError::DecodingError("dP must be less than p")); + } + if nat::sub(d_q, q).1 != 1 { + return Err(SignatureError::DecodingError("dQ must be less than q")); + } + if nat::sub(q_inv, p).1 != 1 { + return Err(SignatureError::DecodingError("qInv must be less than p")); + } + + // `L == 2 * HALF` (checked above), so this widening multiply's `L2 = L` output is exactly + // the modulus width -- `p * q` is exact, never reduced, since it is the modulus itself. + let n = montgomery::widening_mul::(p, q); + + let mut key = Self { + p: Secret::default(), + q: Secret::default(), + d_p: Secret::default(), + d_q: Secret::default(), + q_inv: Secret::default(), + n, + }; + *key.p = *p; + *key.q = *q; + *key.d_p = *d_p; + *key.d_q = *d_q; + *key.q_inv = *q_inv; + Ok(key) + } + + /// The modulus `n = p * q`. + pub fn n(&self) -> &[u64; L] { + &self.n + } + + pub(crate) fn p(&self) -> &[u64; HALF] { + &self.p + } + + pub(crate) fn q(&self) -> &[u64; HALF] { + &self.q + } + + pub(crate) fn d_p(&self) -> &[u64; HALF] { + &self.d_p + } + + pub(crate) fn d_q(&self) -> &[u64; HALF] { + &self.d_q + } + + pub(crate) fn q_inv(&self) -> &[u64; HALF] { + &self.q_inv + } + + /// The `# Encoding` layout (see the type's docs), `p || q || dP || dQ || qInv`, with + /// `HALF_BYTES = 8 * HALF` and `SK_LEN = 5 * HALF_BYTES` supplied by the per-size + /// `SignaturePrivateKey` impl that wraps this. + pub(crate) fn encode_raw(&self) -> [u8; SK_LEN] { + debug_assert_eq!( + HALF_BYTES, + 8 * HALF, + "RsaPrivateKey::encode_raw needs HALF_BYTES == 8 * HALF" + ); + debug_assert_eq!( + SK_LEN, + 5 * HALF_BYTES, + "RsaPrivateKey::encode_raw needs SK_LEN == 5 * HALF_BYTES" + ); + let mut out = [0u8; SK_LEN]; + out[..HALF_BYTES].copy_from_slice(&be_bytes_from_limbs::(&self.p)); + out[HALF_BYTES..2 * HALF_BYTES] + .copy_from_slice(&be_bytes_from_limbs::(&self.q)); + out[2 * HALF_BYTES..3 * HALF_BYTES] + .copy_from_slice(&be_bytes_from_limbs::(&self.d_p)); + out[3 * HALF_BYTES..4 * HALF_BYTES] + .copy_from_slice(&be_bytes_from_limbs::(&self.d_q)); + out[4 * HALF_BYTES..] + .copy_from_slice(&be_bytes_from_limbs::(&self.q_inv)); + out + } + + /// Decodes [`Self::encode_raw`]'s layout, validated the same way + /// [`Self::from_crt_components`] validates its five components. Takes the exact-length array; + /// the per-size `SignaturePrivateKey::from_bytes` that wraps this is where a wrong-length + /// slice is rejected. + pub(crate) fn from_bytes_raw( + bytes: &[u8; SK_LEN], + ) -> Result { + debug_assert_eq!( + HALF_BYTES, + 8 * HALF, + "RsaPrivateKey::from_bytes_raw needs HALF_BYTES == 8 * HALF" + ); + debug_assert_eq!( + SK_LEN, + 5 * HALF_BYTES, + "RsaPrivateKey::from_bytes_raw needs SK_LEN == 5 * HALF_BYTES" + ); + let p_bytes: [u8; HALF_BYTES] = bytes[..HALF_BYTES].try_into().expect("HALF_BYTES bytes"); + let q_bytes: [u8; HALF_BYTES] = + bytes[HALF_BYTES..2 * HALF_BYTES].try_into().expect("HALF_BYTES bytes"); + let d_p_bytes: [u8; HALF_BYTES] = + bytes[2 * HALF_BYTES..3 * HALF_BYTES].try_into().expect("HALF_BYTES bytes"); + let d_q_bytes: [u8; HALF_BYTES] = + bytes[3 * HALF_BYTES..4 * HALF_BYTES].try_into().expect("HALF_BYTES bytes"); + let q_inv_bytes: [u8; HALF_BYTES] = + bytes[4 * HALF_BYTES..].try_into().expect("HALF_BYTES bytes"); + + let p = limbs_from_be_bytes::(&p_bytes); + let q = limbs_from_be_bytes::(&q_bytes); + let d_p = limbs_from_be_bytes::(&d_p_bytes); + let d_q = limbs_from_be_bytes::(&d_q_bytes); + let q_inv = limbs_from_be_bytes::(&q_inv_bytes); + Self::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + } +} + +/// The security strength SP 800-57 Part 1 Rev. 5 Table 2 assigns a modulus of `limbs` 64-bit +/// limbs: 2048 bits give 112, 3072 give 128 (4096 rounds down to the same), 7680 and above give +/// 192; anything narrower is below the table and reports [`SecurityStrength::None`]. +pub const fn modulus_strength(limbs: usize) -> SecurityStrength { + let bits = limbs * 64; + if bits >= 7680 { + SecurityStrength::_192bit + } else if bits >= 3072 { + SecurityStrength::_128bit + } else if bits >= 2048 { + SecurityStrength::_112bit + } else { + SecurityStrength::None + } +} diff --git a/crypto/rsa/src/lib.rs b/crypto/rsa/src/lib.rs new file mode 100644 index 000000000..f12c5206b --- /dev/null +++ b/crypto/rsa/src/lib.rs @@ -0,0 +1,332 @@ +//! RSA digital signatures, RSASSA-PKCS1-v1_5 and RSASSA-PSS (RFC 8017 §8), and the RSASVE +//! key-encapsulation mechanism (NIST SP 800-56B Rev. 2 §7.2.1), built on +//! [`bouncycastle_ec`]'s constant-time limb arithmetic ([`bouncycastle_ec::nat`], +//! [`bouncycastle_ec::montgomery`]) rather than a dedicated bignum type, for the same +//! constant-time reasoning that motivates that crate's own custom-curve arithmetic: a modular +//! exponentiation over a secret exponent has to run in the same time regardless of the exponent's +//! value, which needs limbs of a fixed width known at compile time. +//! +//! # Scope +//! +//! Signature schemes, plus RSASVE as a KEM ([`rsasve`]; see below). There is no RSAES-OAEP or +//! RSAES-PKCS1-v1_5 encryption, and so no KTS-OAEP. Modulus sizes 2048 +//! through 8192 bits ([`rsa_2048`], [`rsa_3072`], [`rsa_4096`], [`rsa_8192`]) support both signing +//! and verification; 1024- and 1536-bit moduli ([`rsa_1024`], [`rsa_1536`]) support verification +//! only, enforced by the absence of a private-key type (and so of any signing-role impl) for +//! those sizes rather than a runtime check. +//! +//! Every (scheme, hash, modulus size) pairing is an algorithm marker type +//! (`RSASSA_PKCS1_v1_5_SHA256` and siblings in each size module) that the key role traits of +//! [`bouncycastle_core::signature_key`] are implemented over: the private key signs through +//! [`SigningKey`](bouncycastle_core::signature_key::SigningKey) for RSASSA-PSS (a fresh salt from +//! the caller's RNG per signer) and +//! [`DeterministicSigningKey`](bouncycastle_core::signature_key::DeterministicSigningKey) for +//! RSASSA-PKCS1-v1_5, and the public key verifies through +//! [`VerifyingKey`](bouncycastle_core::signature_key::VerifyingKey); one-shot or streaming, the +//! same API shape as this workspace's ECDSA, SM2 and ML-DSA crates. One key type serves every +//! scheme and hash, so a call names the marker: `SigningKey::::sign(&sk, +//! &mut rng, msg, None)`. The key types implement +//! [`SignaturePrivateKey`](bouncycastle_core::traits::SignaturePrivateKey)/[`SignaturePublicKey`](bouncycastle_core::traits::SignaturePublicKey) +//! per size, and each signing size's module offers `keygen(rng)` (FIPS 186-5 Appendix A.1.3, in +//! [`keygen`]), as the ECDSA crate's curves do. The generic `rsassa_*` modules also expose +//! `*_from_hash` entry points that take the message hash rather than the message, for a caller +//! that already holds the digest. +//! +//! RSASVE (SP 800-56B Rev. 2 §7.2.1; BC FIPS Java's `FipsRSA.KTS_SVE`, JCA `RSA-KAS-KEM`; +//! OpenSSL's `"RSASVE"` KEM operation) is `RSASVE` in each of the 2048/3072/4096/8192 size modules. +//! It implements [`bouncycastle_core`]'s +//! [`KEMEncapsulator`](bouncycastle_core::traits::KEMEncapsulator)/[`KEMDecapsulator`](bouncycastle_core::traits::KEMDecapsulator) +//! over its own key types (`RSA2048KEMPrivateKey`/`RSA2048KEMPublicKey` and siblings), which are +//! kept apart from the signing key types; see [`rsasve`]'s `# Key separation`. Key generation is +//! `RSASVE::keygen(rng)`, the same FIPS 186-5 generator the signature keys use. The +//! shared secret is RSASVE's raw secret value `Z`, the full modulus length. It is not a derived key. +//! +//! # Usage Examples +//! +//! RSA-2048 with SHA-256 through the `bouncycastle_core` key role traits ([`rsa_2048`]), the way +//! code written against those traits (and this workspace's CLI) uses every signature scheme: +//! +//! ``` +//! use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +//! use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey, SignatureSigner, SignatureVerifier}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! use bouncycastle_rsa::rsa_2048::{RSASSA_PKCS1_v1_5_SHA256, RSASSA_PSS_SHA256, RSA2048PrivateKey, RSA2048PublicKey, SIG_LEN}; +//! # // A genuine RSA-2048 keypair (Wycheproof rsa_pkcs1_2048_sig_gen_test.json's first SHA-256 +//! # // group; `tests/rsa_2048_pkcs1_v1_5_tests.rs` has the full, sourced provenance). A fresh +//! # // pair comes from `bouncycastle_rsa::rsa_2048::keygen(&mut rng)?` instead -- a few seconds of +//! # // Miller-Rabin in a debug build, so this example uses a fixed one. +//! # const P: [u64; 16] = [0x0ea36cfb3a5b18f1, 0x48a6e65332119129, 0x110ad9e7b48a1c93, 0x569156b90113e2e9, 0xe79813a575cfad9c, 0x69d659d143ec6f17, 0xe81e6bab5ddaa783, 0xbff1c5b80a69f788, 0x978f6c35814f50ee, 0xe6a289ad4cfbf78f, 0x34d5681e5809d415, 0xbb028bda42eeb5d2, 0x41c56e4de086b0d5, 0x58b8d1e24f3b55d0, 0xfb5248247d98cb7d, 0xdc431050f782e894]; +//! # const Q: [u64; 16] = [0x669f140cfbc20f25, 0xb97bb03677207d95, 0xfd4e06f3ed7299d4, 0x160f90536abc9492, 0xf5b131f39098f7bc, 0xae8d72c57088d7ab, 0x89b94fbde542aba9, 0x3d3f9880ec47d5e0, 0x1378a6868af3b7a0, 0x5544070beb057c94, 0x16611debc472fac4, 0xe500ffb79f5b8868, 0x308a5e32196603b2, 0xea5fb19eb4eabc38, 0x122273ae3222b598, 0xbd1a81e7977f9898]; +//! # const D_P: [u64; 16] = [0x209f33f09515d7c1, 0xb4a9b37656917205, 0x276933bb07e4efb9, 0x8c14019808e00414, 0x289f96da220711e5, 0xfbbd2923d31532fe, 0xc06b414e61c0e1e7, 0x4c23c4588488961d, 0x4dc48ae34514759c, 0x9c786961ae3e2c35, 0x497e8d9c650688e0, 0x18bf08472612dbe5, 0x8885fb161870ee12, 0xf21d7c1479d99d47, 0x9121d91952ffd1c7, 0xa94b528b28f29159]; +//! # const D_Q: [u64; 16] = [0xf7597ffb68011d8d, 0x7b3cc538c4bab8c9, 0xa8fa480a81a925af, 0x6d6ede7251a383bf, 0x8a63f788ce3a0f85, 0x0b920502eb478bc9, 0x7e37e755edfe70d9, 0x9cf9948422a16555, 0x0d6d9ea1f2ef71fd, 0xf7efa32ea0cb6e00, 0x0629b114ca7f780f, 0xcf51176359654348, 0x540cdcbd4ad35435, 0x31c02ff1a2bc437c, 0xff2503df78bafed5, 0x3af0e72a933aef09]; +//! # const Q_INV: [u64; 16] = [0x552fe4bfce945f7b, 0x67e50c999c67247b, 0xfb54ef17be3b2853, 0x241f5921b5ad3983, 0x02de5eccd143cf31, 0x74e45f6fcc60f216, 0xafa5428a74f12708, 0x88d42294b6a2759b, 0xe923e1097c0c562f, 0xc968b48a91c38b5b, 0x933e85179c0320b0, 0x7993d0445f758d51, 0x9bfc042ee0924b1b, 0x41f956d90fa8a793, 0xee7a87b6483a66ee, 0x2640fbfbcfefb163]; +//! let sk = RSA2048PrivateKey::from_crt_components(&P, &Q, &D_P, &D_Q, &Q_INV)?; +//! let pk = RSA2048PublicKey::new(sk.n(), 0x10001)?; +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! +//! // One-shot. RSA has no context string, so `ctx` is `None` (a `Some` is accepted and ignored). +//! // PKCS#1 v1.5 draws nothing, so it is the deterministic role. +//! let signature = DeterministicSigningKey::::sign_deterministic(&sk, b"the message to sign", None)?; +//! VerifyingKey::::verify(&pk, b"the message to sign", None, &signature)?; +//! +//! // Streaming: the message arrives in chunks and is hashed as it goes. +//! let mut signer = DeterministicSigningKey::::create_signer_deterministic(&sk, None)?; +//! signer.do_update(b"the message "); +//! signer.do_update(b"to sign"); +//! let streamed = signer.do_final()?; +//! assert_eq!(streamed, signature); // PKCS#1 v1.5 is deterministic +//! +//! let mut verifier = VerifyingKey::::create_verifier(&pk, None)?; +//! verifier.do_update(b"the message to sign"); +//! verifier.do_verify_final(&streamed)?; +//! +//! // `verify` takes a `&[u8]`: a signature of the wrong length is rejected for its length +//! // (RFC 8017 §8.2.2 step 1), one over a different message as invalid. +//! assert!(VerifyingKey::::verify(&pk, b"the message to sign", None, &signature[1..]).is_err()); +//! assert!(VerifyingKey::::verify(&pk, b"a different message", None, &signature).is_err()); +//! +//! // RSASSA-PSS draws its salt from the RNG handed to the signer, so two signatures differ. +//! let sig_a = SigningKey::::sign(&sk, &mut rng, b"the message to sign", None)?; +//! let sig_b = SigningKey::::sign(&sk, &mut rng, b"the message to sign", None)?; +//! assert_ne!(sig_a, sig_b); +//! VerifyingKey::::verify(&pk, b"the message to sign", None, &sig_a)?; +//! VerifyingKey::::verify(&pk, b"the message to sign", None, &sig_b)?; +//! +//! // A known-answer test replaces the drawn salt with the published one through `set_salt`, +//! // which makes the signature a function of the key and message alone. +//! let mut signer = SigningKey::::create_signer(&sk, &mut rng, None)?; +//! signer.set_salt([0x42u8; 32]); +//! signer.do_update(b"the message to sign"); +//! let sig_d = signer.do_final()?; +//! let mut signer = SigningKey::::create_signer(&sk, &mut rng, None)?; +//! signer.set_salt([0x42u8; 32]); +//! signer.do_update(b"the message to sign"); +//! assert_eq!(signer.do_final()?, sig_d); +//! VerifyingKey::::verify(&pk, b"the message to sign", None, &sig_d)?; +//! +//! // Keys round-trip through the traits' raw fixed-width encoding (see `keys`'s docs). +//! let pk_again = RSA2048PublicKey::from_bytes(&pk.encode())?; +//! assert_eq!(pk_again, pk); +//! let sk_again = RSA2048PrivateKey::from_bytes(&sk.encode())?; +//! assert_eq!(sk_again, sk); +//! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) +//! ``` +//! +//! RSA-1024 verification, the other major usage pattern: no private key, and so no signing role, +//! exists for this size at all (see `# Scope` above), only `VerifyingKey` for signatures produced +//! elsewhere -- here, one of Wycheproof's own genuine `rsa_pkcs1_1024_sig_gen_test.json` +//! signatures (`tests/rsa_1024_tests.rs` has the full, sourced provenance): +//! +//! ``` +//! use bouncycastle_core::signature_key::VerifyingKey; +//! use bouncycastle_rsa::rsa_1024::{RSASSA_PKCS1_v1_5_SHA256, RSA1024PublicKey, SIG_LEN}; +//! # const N: [u64; 16] = [0xd00343468eaacfbf, 0xb7c7044cc202dcca, 0x9686f30f478db649, 0x5179b54951fff6aa, 0xbabb14f550d5d0dd, 0x5405db7c5c8f4cf6, 0x9816e2eda41fd7b9, 0xb31b6abd805bace9, 0xb909dd0f4c6014f2, 0x9c8a5810b6d05990, 0x40760d1f23fe9250, 0x90adb011a919575a, 0x45e48572113cab28, 0xcb9ca9ec12000fc8, 0x91b4fcaf62a14595, 0xac9048a7a4f560af]; +//! let sig = bouncycastle_hex::decode("41339884a9b3940e8488d666bb158063c6a2a2717cae7f564834a876fcbf7098ecf3acbfabf37d38a8e6127b1e313744f1f896e165efdaea0b2e7673867842b9e94db0868ed9a92bcdcb370a4e20ff275c82595e4400a8b9e9f12482f014846b48216f321266ae6ae6338dbcdc41b711e483e6e3e728772e7f9f5ef95c30196b").unwrap(); +//! let msg = [0u8; 20]; // Wycheproof's own genuine message for this signature. +//! let pk = RSA1024PublicKey::new(&N, 0x10001)?; +//! VerifyingKey::::verify(&pk, &msg, None, &sig)?; +//! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) +//! ``` +//! +//! RSASVE through the `bouncycastle_core` KEM traits ([`rsa_2048::RSASVE`]). The shared secret is +//! the raw `Z`, which goes through a KDF, as SP 800-56B's KAS1/KAS2 schemes require, before it is +//! used as a key: +//! +//! ``` +//! use bouncycastle_core::key_material::KeyMaterialTrait; +//! use bouncycastle_core::traits::{KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, KDF}; +//! use bouncycastle_rsa::rsa_2048::{RSASVE, RSA2048KEMPrivateKey, RSA2048KEMPublicKey, CT_LEN}; +//! # const P: [u64; 16] = [0x0ea36cfb3a5b18f1, 0x48a6e65332119129, 0x110ad9e7b48a1c93, 0x569156b90113e2e9, 0xe79813a575cfad9c, 0x69d659d143ec6f17, 0xe81e6bab5ddaa783, 0xbff1c5b80a69f788, 0x978f6c35814f50ee, 0xe6a289ad4cfbf78f, 0x34d5681e5809d415, 0xbb028bda42eeb5d2, 0x41c56e4de086b0d5, 0x58b8d1e24f3b55d0, 0xfb5248247d98cb7d, 0xdc431050f782e894]; +//! # const Q: [u64; 16] = [0x669f140cfbc20f25, 0xb97bb03677207d95, 0xfd4e06f3ed7299d4, 0x160f90536abc9492, 0xf5b131f39098f7bc, 0xae8d72c57088d7ab, 0x89b94fbde542aba9, 0x3d3f9880ec47d5e0, 0x1378a6868af3b7a0, 0x5544070beb057c94, 0x16611debc472fac4, 0xe500ffb79f5b8868, 0x308a5e32196603b2, 0xea5fb19eb4eabc38, 0x122273ae3222b598, 0xbd1a81e7977f9898]; +//! # const D_P: [u64; 16] = [0x209f33f09515d7c1, 0xb4a9b37656917205, 0x276933bb07e4efb9, 0x8c14019808e00414, 0x289f96da220711e5, 0xfbbd2923d31532fe, 0xc06b414e61c0e1e7, 0x4c23c4588488961d, 0x4dc48ae34514759c, 0x9c786961ae3e2c35, 0x497e8d9c650688e0, 0x18bf08472612dbe5, 0x8885fb161870ee12, 0xf21d7c1479d99d47, 0x9121d91952ffd1c7, 0xa94b528b28f29159]; +//! # const D_Q: [u64; 16] = [0xf7597ffb68011d8d, 0x7b3cc538c4bab8c9, 0xa8fa480a81a925af, 0x6d6ede7251a383bf, 0x8a63f788ce3a0f85, 0x0b920502eb478bc9, 0x7e37e755edfe70d9, 0x9cf9948422a16555, 0x0d6d9ea1f2ef71fd, 0xf7efa32ea0cb6e00, 0x0629b114ca7f780f, 0xcf51176359654348, 0x540cdcbd4ad35435, 0x31c02ff1a2bc437c, 0xff2503df78bafed5, 0x3af0e72a933aef09]; +//! # const Q_INV: [u64; 16] = [0x552fe4bfce945f7b, 0x67e50c999c67247b, 0xfb54ef17be3b2853, 0x241f5921b5ad3983, 0x02de5eccd143cf31, 0x74e45f6fcc60f216, 0xafa5428a74f12708, 0x88d42294b6a2759b, 0xe923e1097c0c562f, 0xc968b48a91c38b5b, 0x933e85179c0320b0, 0x7993d0445f758d51, 0x9bfc042ee0924b1b, 0x41f956d90fa8a793, 0xee7a87b6483a66ee, 0x2640fbfbcfefb163]; +//! // The same key material as above, as key-establishment keys. A fresh pair comes from +//! // `RSASVE::keygen()?` instead. +//! let sk = RSA2048KEMPrivateKey::from_crt_components(&P, &Q, &D_P, &D_Q, &Q_INV)?; +//! let pk = RSA2048KEMPublicKey::new(sk.n(), 65537)?; +//! +//! // The encapsulating party: a fresh secret value Z and its ciphertext C = z^e mod n. +//! let (z, ct) = RSASVE::encaps(&pk)?; +//! assert_eq!(ct.len(), CT_LEN); +//! +//! // The private-key holder recovers the same Z from C. +//! let z_again = RSASVE::decaps(&sk, &ct)?; +//! assert_eq!(z_again, z); +//! +//! // Z is a shared secret, not a key: derive the key actually used, here with HKDF-SHA-256 +//! // (SP 800-56C Rev. 2's two-step method), binding in whatever context the protocol defines. +//! let key = bouncycastle_sha2::hkdf::HKDF_SHA256::default() +//! .derive_key(&z, b"example protocol v1") +//! .expect("HKDF accepts any non-empty input key"); +//! assert_eq!(key.key_len(), 32); +//! +//! // A wrong-length ciphertext is an error. So is one outside (1, n - 1) (SP 800-56B §7.1.2.3). +//! assert!(RSASVE::decaps(&sk, &ct[1..]).is_err()); +//! assert!(RSASVE::decaps(&sk, &[0u8; CT_LEN]).is_err()); +//! # Ok::<(), bouncycastle_core::errors::KEMError>(()) +//! ``` +//! +//! RSASSA-PSS-SHAKE128/256 (RFC 8702 §3.2.1: [`rsa_2048::RSASSA_PSS_SHAKE128`], +//! [`rsa_3072::RSASSA_PSS_SHAKE128`], [`rsa_4096::RSASSA_PSS_SHAKE256`], per §5's recommended +//! pairing) have exactly the shape of `RSASSA_PSS_SHA256` above, `set_salt` included -- see +//! [`rsa_2048`]'s own docs rather than a second, near-identical example here. +//! +//! # Status +//! +//! [`modexp`] (constant-time modular exponentiation over a runtime-supplied modulus), [`keys`] +//! (the CRT key types), the CRT-based RSASP1/RSAVP1 primitives (crate-private, in `rsa_core`), +//! RSASSA-PKCS1-v1_5 ([`rsassa_pkcs1_v1_5`]), and RSASSA-PSS ([`rsassa_pss`]) are implemented, +//! with SHA-256, SHA-384, and SHA-512 wired up for every modulus size this crate offers -- 1024 +//! and 1536 (verification only, per `# Scope` above) and 2048/3072/4096/8192 (both directions) -- +//! and validated against genuine vectors: Wycheproof for every size except 8192, which it has no +//! key material for at all (cross-checked against BC Java instead); see each `rsa_*.rs` module's +//! own tests for the specifics at each size. Every pairing also implements `bouncycastle_core`'s +//! `Signer`/`SignatureVerifier` traits (see `# Scope`), exercised through +//! `bouncycastle_core_test_framework`'s shared signature conformance suite at each size. +//! +//! RSASSA-PSS-SHAKE128 and RSASSA-PSS-SHAKE256 (RFC 8702 §3.2.1 -- SHAKE128/SHAKE256 used +//! natively as both the message hash and the mask generation function, in place of a hash+MGF1 +//! split) are also implemented ([`rsassa_pss_shake`], built on `emsa_pss_shake` rather than +//! `mgf1`), wired up for the pairings RFC 8702 §5 recommends: SHAKE128 with RSA-2048/3072 +//! ([`rsa_2048`], [`rsa_3072`]) and SHAKE256 with RSA-4096 ([`rsa_4096`]), each validated against +//! its own genuine Wycheproof `rsa_pss_*_shake*_test.json` vectors. +//! +//! RSASVE ([`rsasve`]) is implemented at 2048/3072/4096/8192. It is validated against NIST +//! ACVP-Server's KAS-IFC-SSC (KAS1 at 2048, KAS2 at 3072, both directions) and SP 800-56B RSADP +//! (CRT keys at 2048/3072/4096, out-of-range ciphertexts included) vectors, and against OpenSSL +//! 3.0's `"RSASVE"` KEM operation at every size, 8192 included. See `tests/rsasve_tests.rs` for +//! the sources. +//! +//! # Memory Usage +//! +//! | Key Object | PK size on disk | PK size in memory | SK size on disk | SK size in memory | +//! |-------------------|------------------|--------------------|------------------|--------------------| +//! | RSA-1024 (verify-only) | 132 | 136 | -- | -- | +//! | RSA-1536 (verify-only) | 196 | 200 | -- | -- | +//! | RSA-2048 | 260 | 264 | 640 | 896 | +//! | RSA-3072 | 388 | 392 | 960 | 1344 | +//! | RSA-4096 | 516 | 520 | 1280 | 1792 | +//! | RSA-8192 | 1028 | 1032 | 2560 | 3584 | +//! +//! The RSASVE key types (`RSAKEMPublicKey`/`RSAKEMPrivateKey`) have the same encodings +//! and the same in-memory sizes as the signature key types above: each wraps one. An RSASVE +//! ciphertext and shared secret are each `nLen` bytes, the same as a signature (256/384/512/1024 +//! at 2048/3072/4096/8192). +//! +//! Peak stack usage, as measured by `bench_rsa_mem_usage` under `valgrind --tool=massif +//! --stacks=yes` (release build, x86-64): +//! +//! | Operation | RSA-2048 | RSA-8192 | +//! |----------------------------|----------|----------| +//! | PKCS#1 v1.5 sign (SHA-256) | 10120 | 34216 | +//! | RSASVE encaps | 10696 | 35464 | +//! | RSASVE decaps | 10744 | 40776 | +//! +//! All values are in bytes. "On disk" is the `SignaturePublicKey`/`SignaturePrivateKey` raw +//! layout ([`keys::RsaPublicKey`]'s and [`keys::RsaPrivateKey`]'s `# Encoding`: `n || e`, and +//! `p || q || dP || dQ || qInv` respectively); "in memory" is `core::mem::size_of`. A signature +//! is the same size as the modulus itself (`K_LEN = 8 * L` bytes: 128/192/256/384/512/1024 for +//! 1024/1536/2048/3072/4096/8192). These numbers are produced by `mem_usage_benches`' +//! `bench_rsa_mem_usage` binary's `print_key_sizes()`; that binary is also the stack-usage +//! measurement harness (see its own doc comment for the `valgrind`/`massif` invocation) -- only +//! one representative (scheme, hash) pairing is profiled there per modulus size, since stack +//! usage is dominated by the modulus's own limb count `L`, not by which hash or scheme fed into +//! RSASP1/RSAVP1's modular exponentiation. A `Signer`/`SignatureVerifier` streaming state +//! (`RSASSA_*` in each size module) additionally holds a clone of its key for its lifetime -- +//! the "SK size in memory" column, for a signer -- plus the hash state. +//! +//! # Security Considerations +//! +//! - **RSASP1 (private-key signing) is not blinded against its input.** [`modexp`]'s `mod_pow` is +//! constant-time in the *exponent* (the CRT exponents `dP`/`dQ`), which defeats a pure timing +//! attack, but neither it nor `rsa_core::rsasp1` multiplies the message by a random blinding +//! factor before exponentiating and removes it afterward. An attacker able to mount a +//! chosen-message physical side-channel attack (power or cache analysis, not just wall-clock +//! timing) against many signatures under the same key may still be able to recover information +//! about `p`/`q`/`dP`/`dQ`, per Kocher (1996) and Boneh & Brumley (2003) -- see [`modexp`]'s own +//! docs for the exact boundary of what is and is not covered. +//! - **Imported keys are not validated for primality.** [`keys::RsaPrivateKey::from_crt_components`] +//! checks that `p`/`q` are odd and distinct and that `dP`/`dQ`/`qInv` are in range, but does +//! not test `p` and `q` for primality or `e` for coprimality with `λ(n)`. Supplying non-prime or +//! otherwise malformed CRT components produces a key that computes *something*, silently, +//! rather than being rejected -- validating imported key material's number-theoretic properties +//! before construction is the caller's responsibility (`keygen::is_probable_prime` is available +//! for the primality part). Keys from this crate's own `keygen` meet FIPS 186-5 Appendix A.1.1's +//! criteria by construction. +//! - **Key generation is not constant-time.** Rejection sampling of prime candidates is +//! inherently data-dependent in how many candidates it draws and where each is rejected, and the +//! small-modulus arithmetic on candidates uses ordinary integer division -- see [`keygen`]'s docs +//! for exactly what is and is not constant-time there. Signing itself is unaffected. +//! - **RSASSA-PSS's salt is only as good as its source.** `SigningKey::create_signer` draws it +//! from the RNG it is handed, once per signer; `set_salt` on the signer takes the salt itself. Unlike ECDSA's per-message secret `k`, a repeated +//! or predictable PSS salt does not hand an attacker the private key by itself (there is no +//! algebraic relation analogous to ECDSA's nonce-reuse equation), but PSS's random-oracle +//! security argument (RFC 8017 §8.1, Appendix A.2.3's references) assumes the salt is drawn +//! fresh and unpredictably each time. Signing with a low-quality or unseeded RNG, or reusing a +//! fixed salt outside a test, weakens that argument even though it will not directly leak `d`. +//! - **`ctx` is accepted but ignored.** RSASSA-PKCS1-v1_5 and RSASSA-PSS (RFC 8017 §8) have no +//! context-string input; the key traits' `ctx` parameter is accepted for trait +//! conformance and silently ignored, as `bouncycastle-ecdsa` does for ECDSA. A caller relying +//! on `ctx` to bind a signature to an application context gets no such binding here and must +//! fold the context into the message itself. +//! - **RSA-1024 and RSA-1536 are legacy sizes, exposed for verification only.** Both fall below +//! the 112-bit security level NIST SP 800-131A requires for new use (RSA-1024 offers roughly +//! 80 bits, RSA-1536 roughly 96); this crate offers no way to sign with either (see `# Scope`) +//! precisely so they can only be used to check signatures against legacy/third-party data, not +//! to produce new ones. +//! - **RSASVE's shared secret is the raw secret value `Z`, not a key.** SP 800-56B Rev. 2 uses +//! RSASVE only inside KAS1/KAS2, which always pass `Z` through a key-derivation method (§5.5, +//! SP 800-56C) together with the parties' identifiers and other context. `Z` is uniform on +//! `(1, n - 1)`, not on all `nLen`-byte strings, and nothing binds it to either party or to the +//! protocol. Use it only as KDF input. +//! - **RSASVE decapsulation does not authenticate the ciphertext.** Any `C` in `(1, n - 1)` +//! recovers *some* `Z`: RSASVE has no redundancy to check, so a corrupted or forged ciphertext +//! yields a different secret, not an error. Assurance that both sides hold the same key has to +//! come from key confirmation (SP 800-56B §5.6) or from the protocol's later use of the derived +//! key, and none of that is provided here. +//! - **RSASVE decapsulation is RSADP on attacker-chosen input, and it is not blinded.** The +//! RSASP1 bullet above applies here with more force: a ciphertext is chosen by whoever sends +//! it, so an attacker who can observe a physical side channel during decapsulation needs no +//! more than the ability to submit ciphertexts. SP 800-56B §7.2.1.3's note (Håstad-Näslund +//! bit security) is the related concern: the recovered `Z` must not leak through timing either. +//! `decaps` itself is constant-time in `Z`, but what the caller does with `Z` is up to the +//! caller. +//! - **RSASVE public exponents are limited to 32 bits.** SP 800-56B §6.2.1 allows +//! `65,537 <= e < 2^256`, but [`keys::RsaPublicKey`] holds `e` as a `u32`, so an RSASVE public +//! key with a larger `e` cannot be constructed. (ACVP's RSADP vectors use exponents of up to 56 +//! bits, which is why only their decapsulation side is tested here: decapsulation never needs +//! `e`.) +//! - **RSASVE key validation is partial.** The KEM key types check `e >= 65,537` and that `n` has +//! exactly the size's bit length, on top of the signature keys' checks. They do not run SP +//! 800-89's remaining plausibility tests, and full RSA public-key validation is not specified by +//! SP 800-56B at all. See [`rsasve`]'s `# Key validation`. +//! - **RSASSA-PSS's salt length is fixed to the hash's own output length** (RFC 8017 §9.1 note +//! 4's "typical" choice) rather than being caller-configurable. A verifier that expects a +//! different salt length (including the zero-length salt some other implementations default +//! to) will not interoperate with signatures this crate produces, and this crate's own verifier +//! will reject signatures made with a different salt length. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +mod codec; +mod emsa_pkcs1_v1_5; +mod emsa_pss; +mod emsa_pss_shake; +pub mod keygen; +pub mod keys; +mod mgf1; +pub mod modexp; +pub mod rsa_1024; +pub mod rsa_1536; +pub mod rsa_2048; +pub mod rsa_3072; +pub mod rsa_4096; +pub mod rsa_8192; +mod rsa_core; +pub mod rsassa_pkcs1_v1_5; +pub mod rsassa_pss; +pub mod rsassa_pss_shake; +pub mod rsasve; diff --git a/crypto/rsa/src/mgf1.rs b/crypto/rsa/src/mgf1.rs new file mode 100644 index 000000000..0c36b7613 --- /dev/null +++ b/crypto/rsa/src/mgf1.rs @@ -0,0 +1,91 @@ +//! MGF1 (RFC 8017 Appendix B.2.1): a mask generation function based on a hash function, used by +//! [`crate::emsa_pss`]. Specialized to PSS's own call shape (`MGF(H, maskLen)`, where `H` is +//! always the hash's own `hLen`-byte output) rather than MGF1's fully general arbitrary-length +//! seed: `mgfSeed` here is always exactly `H_LEN` bytes. + +use bouncycastle_core::traits::HashOneShot; + +/// MGF1 (RFC 8017 Appendix B.2.1), with `mgfSeed` fixed at `H_LEN` bytes and `maskLen` fixed at +/// `MASK_LEN` (a compile-time constant here, since every call site in this crate knows its output +/// width in advance -- `emLen - hLen - 1` for a concrete modulus/hash pairing). Step 1's error +/// ("mask too long", `maskLen > 2^32 hLen`) cannot occur for any `MASK_LEN` this crate reaches +/// (at most a few KiB, for an 8192-bit modulus), so it is not checked. `SEED_LEN` is `H_LEN + 4` +/// (the seed plus the 4-octet counter `C`), a separate const parameter for the same reason +/// [`bouncycastle_ec::montgomery`]'s `L2 = 2 * L` is: stable Rust cannot compute one const +/// generic from another. +pub fn mgf1( + seed: &[u8; H_LEN], +) -> [u8; MASK_LEN] { + debug_assert_eq!(SEED_LEN, H_LEN + 4, "mgf1 needs SEED_LEN == H_LEN + 4"); + + let mut mask = [0u8; MASK_LEN]; + let mut offset = 0; + let mut counter: u32 = 0; + while offset < MASK_LEN { + let mut input = [0u8; SEED_LEN]; + input[..H_LEN].copy_from_slice(seed); + input[H_LEN..].copy_from_slice(&counter.to_be_bytes()); + + let mut block = [0u8; H_LEN]; + H::hash_out(&input, &mut block); + + let take = core::cmp::min(H_LEN, MASK_LEN - offset); + mask[offset..offset + take].copy_from_slice(&block[..take]); + offset += take; + counter += 1; + } + mask +} + +#[cfg(test)] +mod tests { + //! `mgf1` is crate-private (only [`crate::emsa_pss`] needs it), so it is exercised here + //! rather than from `tests/` -- the same "high-risk code that cannot be reached through the + //! public API" exception `rsa_core`'s tests use. Every expected value below was computed with + //! a direct Python transliteration of RFC 8017 Appendix B.2.1's own steps (not from recall), + //! using `hashlib.sha256`. + + use super::*; + use bouncycastle_sha2::SHA256; + + const SEED: [u8; 32] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, + 0x1e, 0x1f, + ]; + + /// `MASK_LEN = 223`: the exact `DB_LEN` RSA-2048/SHA-256 PSS uses (`K_LEN - H_LEN - 1 = 256 - + /// 32 - 1`), spanning seven full 32-byte hash blocks (`ceil(223/32) = 7`) plus a truncated one. + #[test] + fn mgf1_matches_python_for_pss_2048_sha256_mask_length() { + let mask: [u8; 223] = mgf1::(&SEED); + let expected_prefix: [u8; 16] = [ + 0x70, 0xf4, 0x00, 0x3d, 0x52, 0xb6, 0xeb, 0x03, 0xda, 0x85, 0x2e, 0x93, 0x25, 0x6b, + 0x59, 0x86, + ]; + assert_eq!(&mask[..16], &expected_prefix); + let expected_suffix: [u8; 8] = [0xff, 0x1f, 0x1e, 0x99, 0x50, 0x22, 0x99, 0x60]; + assert_eq!(&mask[215..223], expected_suffix); + } + + /// `MASK_LEN = 10`, shorter than one hash block: exercises truncation of the very first block. + #[test] + fn mgf1_truncates_within_the_first_block() { + let mask: [u8; 10] = mgf1::(&SEED); + let expected: [u8; 10] = [0x70, 0xf4, 0x00, 0x3d, 0x52, 0xb6, 0xeb, 0x03, 0xda, 0x85]; + assert_eq!(mask, expected); + // Must agree with the longer mask's own prefix -- MGF1 is a genuine stream, not + // recomputed independently per length. + let longer: [u8; 223] = mgf1::(&SEED); + assert_eq!(mask, longer[..10]); + } + + /// `MASK_LEN = 70`, spanning three counter blocks (`ceil(70/32) = 3`) with truncation of the + /// last. + #[test] + fn mgf1_spans_multiple_counter_blocks() { + let mask: [u8; 70] = mgf1::(&SEED); + let longer: [u8; 223] = mgf1::(&SEED); + assert_eq!(mask, longer[..70]); + } +} diff --git a/crypto/rsa/src/modexp.rs b/crypto/rsa/src/modexp.rs new file mode 100644 index 000000000..3a73531c9 --- /dev/null +++ b/crypto/rsa/src/modexp.rs @@ -0,0 +1,282 @@ +//! Constant-time modular exponentiation over a runtime-supplied odd modulus, built on +//! [`bouncycastle_ec::nat`] and [`bouncycastle_ec::montgomery`]. +//! +//! Every curve in `bouncycastle-ec` treats its modulus as a per-curve compile-time constant, with +//! the Montgomery constant `n'` computed once, offline, and hard-coded. RSA's modulus is key +//! material supplied at runtime, so this module computes the Montgomery setup itself: +//! +//! * [`mont_n_prime`] derives `n' = -n0^-1 mod 2^64` from the modulus's low limb by Newton's +//! method (six doublings of precision from a one-bit-correct seed), rather than requiring it +//! precomputed. +//! * [`MontgomeryContext::new`] additionally derives `R mod n` and `R^2 mod n` (`R = 2^(64L)`) by +//! repeated doubling-and-reduction, rather than requiring them precomputed. +//! +//! Both of these operate on the modulus alone, which is public in RSA (shared between the public +//! and private key), so they run in variable time -- the same reasoning +//! [`bouncycastle_ec::inverse_vartime`] documents for operating on public curve/scalar values. +//! [`mod_pow`] is the one piece here that operates on a value that can be secret (the private +//! exponent `d`): it processes every one of the modulus's `64 * L` bits regardless of the +//! exponent's actual value or bit length, using Joye & Yen's Montgomery powering ladder (CHES +//! 2002, "The Montgomery Powering Ladder"), so it performs exactly one multiplication and one +//! squaring per bit, moved into place by a branch-free conditional swap rather than a branch on +//! the bit's value. +//! +//! # Security Considerations +//! +//! [`mod_pow`] is constant-time in the *exponent*, which is what RSA signing needs (`d` is +//! secret). It is not blinded against the message/base: an attacker who can also mount a +//! chosen-message physical side-channel attack (power/cache analysis, not just timing) may still +//! learn about `d` from repeated calls with related bases, per Kocher (1996) and Boneh & Brumley +//! (2003). Neither this module nor [`crate::rsa_core`]'s CRT-based `rsasp1` (the only caller that +//! exponentiates with a secret exponent) applies message blinding on top of it -- see the crate's +//! own `# Security Considerations` for what that means for this crate's signing operation. + +use bouncycastle_ec::montgomery; +use bouncycastle_ec::nat; +use bouncycastle_utils::ct::Condition; + +/// `n' = -n0^-1 mod 2^64` for an odd `n0`, by Newton's method: `x` is correct to one bit at +/// `x = 1` (since `n0` is odd, `n0 * 1` is odd, i.e. `≡ 1 mod 2`), and each round of +/// `x <- x * (2 - n0 * x)` (all mod `2^64`, via wrapping arithmetic) doubles the number of +/// correct low bits, so six rounds carry it from one correct bit to all sixty-four +/// (`1, 2, 4, 8, 16, 32, 64`). +pub fn mont_n_prime(n0: u64) -> u64 { + debug_assert_eq!(n0 & 1, 1, "mont_n_prime needs an odd modulus"); + let mut inv = 1u64; + for _ in 0..6 { + inv = inv.wrapping_mul(2u64.wrapping_sub(n0.wrapping_mul(inv))); + } + inv.wrapping_neg() +} + +/// `(a * 2) mod n` for `a < n` and odd `n`, in variable time: doubling `a` can carry out of the +/// top limb or land in `[n, 2n)`, either of which means exactly one subtraction of `n` restores +/// the `< n` representative (it cannot need a second: `a < n` implies `2a < 2n`). +fn double_mod_n(a: &[u64; L], n: &[u64; L]) -> [u64; L] { + let (doubled, carry) = nat::add(a, a); + let (diff, borrow) = nat::sub(&doubled, n); + if carry == 1 || borrow == 0 { diff } else { doubled } +} + +/// `2^bits mod n` for odd `n`, by `bits` repeated doublings from `1`. Variable time, like +/// [`double_mod_n`]: the modulus is public. +fn pow2_mod(bits: u32, n: &[u64; L]) -> [u64; L] { + let mut acc = [0u64; L]; + acc[0] = 1; + for _ in 0..bits { + acc = double_mod_n(&acc, n); + } + acc +} + +/// Per-limb constant-time select: `mask.select` applied element-wise. +fn ct_select_array(mask: Condition, a: &[u64; L], b: &[u64; L]) -> [u64; L] { + let mut result = [0u64; L]; + for i in 0..L { + result[i] = mask.select(a[i], b[i]); + } + result +} + +/// Per-limb constant-time swap, in place. +fn ct_swap_array(mask: Condition, a: &mut [u64; L], b: &mut [u64; L]) { + for i in 0..L { + let (x, y) = mask.swap(a[i], b[i]); + a[i] = x; + b[i] = y; + } +} + +/// A Montgomery-form value of `< 2n` (the bound [`montgomery::redc`] returns), plus the extra bit +/// beyond its `L` limbs, reduced to the canonical `< n` representative. +/// +/// `extra` and `high` are never both large enough to matter independently: if `extra == 1` then +/// `high + 2^(64L) >= 2^(64L) > n`, so the value is always `>= n` in that case and a subtraction +/// mask of `extra == 1` alone would be correct for it; the `borrow == 0` (`high >= n`) arm handles +/// the remaining `extra == 0` case, so the two conditions ORed together cover exactly the "value +/// `>= n`" set. +fn redc_normalize( + t: &[u64; L2], + modulus: &[u64; L], + n_prime: u64, +) -> [u64; L] { + let (high, extra) = montgomery::redc::(t, modulus, n_prime); + let (diff, borrow) = nat::sub(&high, modulus); + // Mutating this `|` to `^` is an accepted equivalent, not a gap: the two masks are never both + // TRUE at once. `extra == 1` forces `high < n` (equivalently `borrow == 1`, so the second mask + // is FALSE) -- see the doc comment above -- because every caller's `t` comes from + // `montgomery::redc`'s own `< 2n` bound, never from an unconstrained 2L-limb value. + let subtract = Condition::::from_lsb(extra) | Condition::::from_lsb(borrow ^ 1); + ct_select_array(subtract, &diff, &high) +} + +fn mont_mul( + a: &[u64; L], + b: &[u64; L], + modulus: &[u64; L], + n_prime: u64, +) -> [u64; L] { + let t = montgomery::widening_mul::(a, b); + redc_normalize::(&t, modulus, n_prime) +} + +fn mont_square( + a: &[u64; L], + modulus: &[u64; L], + n_prime: u64, +) -> [u64; L] { + let t = montgomery::widening_square::(a); + redc_normalize::(&t, modulus, n_prime) +} + +fn to_montgomery( + a: &[u64; L], + ctx: &MontgomeryContext, +) -> [u64; L] { + let t = montgomery::widening_mul::(a, &ctx.r2_mod_n); + redc_normalize::(&t, &ctx.modulus, ctx.n_prime) +} + +fn from_montgomery( + a_bar: &[u64; L], + ctx: &MontgomeryContext, +) -> [u64; L] { + let mut padded = [0u64; L2]; + padded[..L].copy_from_slice(a_bar); + redc_normalize::(&padded, &ctx.modulus, ctx.n_prime) +} + +/// `a * b mod ctx.modulus()`, for plain (non-Montgomery) `a`, `b`. Only `b` needs converting into +/// Montgomery form: `REDC(a * (b*R)) = a*b*R*R^-1 mod n = a*b mod n`, the same one-sided-conversion +/// trick [`mod_pow`] itself doesn't need (it stays in Montgomery form throughout its ladder). +/// `a` need only fit in `L` limbs, not be `< ctx.modulus()`, by the same `T < n*R` argument +/// [`to_montgomery`] relies on -- see the module docs' note on [`montgomery::redc`]'s precondition. +pub fn mul_mod( + a: &[u64; L], + b: &[u64; L], + ctx: &MontgomeryContext, +) -> [u64; L] { + let b_bar = to_montgomery::(b, ctx); + let t = montgomery::widening_mul::(a, &b_bar); + redc_normalize::(&t, &ctx.modulus, ctx.n_prime) +} + +/// `(a - b) mod modulus` for `a, b < modulus`, in constant time with respect to `modulus`: RSA's +/// CRT recombination (RFC 8017 §5.2.1 step 2.b.3) needs `(s1 - s2) mod p`, and `p` is private key +/// material, so -- unlike [`double_mod_n`]'s public-modulus reasoning -- this must not branch on +/// it. +pub fn sub_mod(a: &[u64; N], b: &[u64; N], modulus: &[u64; N]) -> [u64; N] { + let (diff, borrow) = nat::sub(a, b); + let (corrected, _) = nat::add(&diff, modulus); + let mask = Condition::::from_lsb(borrow); + ct_select_array(mask, &corrected, &diff) +} + +/// `(acc * 2 + bit) mod modulus` for `acc < modulus` and odd `modulus`, in constant time: see +/// [`reduce_wide`] for why (the modulus can be secret, unlike [`pow2_mod`]'s). +fn ct_shift_in_bit_mod(acc: &[u64; N], bit: u64, modulus: &[u64; N]) -> [u64; N] { + let mut doubled = [0u64; N]; + let mut carry = bit; + for i in 0..N { + let next_carry = acc[i] >> 63; + // Mutating this `|` to `^` is an accepted equivalent, not a gap: `acc[i] << 1` always has + // bit 0 clear (a left shift's incoming bit), and `carry` is 0 or 1, so OR-ing it into that + // always-clear bit and XOR-ing it produce identical results. + doubled[i] = (acc[i] << 1) | carry; + carry = next_carry; + } + // Same accepted OR/XOR equivalence as `redc_normalize`, by the identical argument: `doubled`'s + // true value is `2*acc + bit < 2*modulus` (`acc < modulus`), so `carry == 1` (the bit that + // overflowed the top limb) forces the stored `doubled < 2*modulus - 2^(64N) < modulus`, i.e. + // `borrow == 1` (the other mask FALSE) whenever `carry == 1`. + let (diff, borrow) = nat::sub(&doubled, modulus); + let subtract = Condition::::from_lsb(carry) | Condition::::from_lsb(borrow ^ 1); + ct_select_array(subtract, &diff, &doubled) +} + +/// `value mod modulus` for a `WIDE`-limb `value` and a `NARROW`-limb odd `modulus`, in constant +/// time with respect to `modulus`. Unlike [`pow2_mod`]/[`double_mod_n`], which assume a *public* +/// modulus (true of RSA's own `n`), this brings a value down to a CRT prime's width and the primes +/// are private key material: branching on their value here would leak them the same way branching +/// on the exponent would leak `d`. Used two ways in RSA's CRT recombination (RFC 8017 §5.2.1 step +/// 2.b): with `WIDE > NARROW`, to reduce the `n`-width message `m` down to `m mod p` before +/// exponentiating (step 2.b.1); and with `WIDE == NARROW`, to reduce `s2` (a residue mod `q`) down +/// to `s2 mod p` before the CRT subtraction (step 2.b.3) -- `s2 < q` gives no useful bound +/// relative to `p` in general (RFC 8017 does not require `p` and `q` to be close in magnitude), +/// so this does not special-case a single conditional subtraction the way [`double_mod_n`] does +/// for the (public, and by construction `< n`) doubling case. Bit-serial schoolbook long division: +/// every one of `value`'s `64 * WIDE` bits contributes one constant-time doubling-and-conditional- +/// subtract step. +pub fn reduce_wide( + value: &[u64; WIDE], + modulus: &[u64; NARROW], +) -> [u64; NARROW] { + let mut acc = [0u64; NARROW]; + for limb_index in (0..WIDE).rev() { + for bit_index in (0..64u32).rev() { + let bit = (value[limb_index] >> bit_index) & 1; + acc = ct_shift_in_bit_mod(&acc, bit, modulus); + } + } + acc +} + +/// Precomputed Montgomery constants for a runtime-supplied odd modulus `n`, `L` limbs wide. +/// +/// Unlike every curve modulus in `bouncycastle-ec`, `n` here is not known until the key is +/// loaded, so these constants (see the module docs) are derived at construction time rather than +/// hard-coded. +pub struct MontgomeryContext { + modulus: [u64; L], + n_prime: u64, + /// Montgomery form of `1`, i.e. `R mod n`. + r_mod_n: [u64; L], + /// `R^2 mod n`, used to bring a value into Montgomery form. + r2_mod_n: [u64; L], +} + +impl MontgomeryContext { + /// Builds the Montgomery context for `modulus`. Returns `None` if `modulus` is even or zero; + /// Montgomery reduction requires an odd modulus, and RSA's is always odd (a product of two + /// odd primes). + pub fn new(modulus: &[u64; L]) -> Option { + if modulus[0] & 1 == 0 || nat::is_zero(modulus).to_bool() { + return None; + } + let n_prime = mont_n_prime(modulus[0]); + let r_mod_n = pow2_mod(64 * L as u32, modulus); + let r2_mod_n = pow2_mod(128 * L as u32, modulus); + Some(Self { modulus: *modulus, n_prime, r_mod_n, r2_mod_n }) + } +} + +/// `base^exponent mod ctx.modulus()`, constant-time in `exponent`: every one of the modulus's +/// `64 * L` bits is processed, via Joye & Yen's Montgomery powering ladder (see the module docs). +/// +/// `base` must be `< ctx.modulus()`; callers reduce first if that is not already guaranteed (RSA +/// callers always pass an already-reduced value: a message digest padded to the modulus width, or +/// a signature, both taken modulo `n` by construction). +pub fn mod_pow( + base: &[u64; L], + exponent: &[u64; L], + ctx: &MontgomeryContext, +) -> [u64; L] { + debug_assert_eq!(nat::sub(base, &ctx.modulus).1, 1, "mod_pow needs base < modulus"); + + let mut r0 = ctx.r_mod_n; + let mut r1 = to_montgomery::(base, ctx); + + for limb_index in (0..L).rev() { + for bit_index in (0..64u32).rev() { + let mask = Condition::::is_bit_set(exponent[limb_index], bit_index); + ct_swap_array(mask, &mut r0, &mut r1); + let new_r1 = mont_mul::(&r0, &r1, &ctx.modulus, ctx.n_prime); + let new_r0 = mont_square::(&r0, &ctx.modulus, ctx.n_prime); + r0 = new_r0; + r1 = new_r1; + ct_swap_array(mask, &mut r0, &mut r1); + } + } + + from_montgomery::(&r0, ctx) +} diff --git a/crypto/rsa/src/rsa_1024.rs b/crypto/rsa/src/rsa_1024.rs new file mode 100644 index 000000000..b2106569e --- /dev/null +++ b/crypto/rsa/src/rsa_1024.rs @@ -0,0 +1,75 @@ +//! RSA-1024: verification only. `L = 16` (1024 bits). There is no `RSA1024PrivateKey` and no +//! signing function -- per this crate's `# Scope` (see `src/lib.rs`), a 1024-bit modulus is for +//! verifying signatures made elsewhere (legacy interoperability), not for producing new ones, and +//! that is enforced by the absent type rather than a runtime check. +//! +//! Only SHA-256 and SHA-384 PKCS#1 v1.5 are wired up: Wycheproof's own +//! `rsa_pkcs1_1024_sig_gen_test.json` has no SHA-512 group at 1024 bits (unlike every other size +//! this crate offers) and no PSS vectors at all, so there is nothing to validate a SHA-512 or PSS +//! pairing against at this size -- both remain mathematically expressible through +//! [`crate::rsassa_pkcs1_v1_5`]/[`crate::rsassa_pss`] directly if a caller ever needs them, just +//! not exposed here without real vectors behind them. +//! +//! The same verification-only scope holds for the `bouncycastle_core` trait types +//! ([`RSASSA_PKCS1_v1_5_SHA256`], [`RSASSA_PKCS1_v1_5_SHA384`]): [`RSA1024PublicKey`] implements +//! `VerifyingKey` for them, but no key implements `DeterministicSigningKey`, because the signing +//! role is only implemented where the private key type implements `SignaturePrivateKey`, and +//! `RsaPrivateKey<16, 8>` does not (no size module declares it). Enforced by the compiler, as +//! this doctest pins: +//! +//! ```compile_fail,E0277 +//! use bouncycastle_core::signature_key::DeterministicSigningKey; +//! use bouncycastle_rsa::keys::RsaPrivateKey; +//! use bouncycastle_rsa::rsa_1024::{RSASSA_PKCS1_v1_5_SHA256, SIG_LEN}; +//! +//! fn sign_1024(sk: &RsaPrivateKey<16, 8>) { +//! // error: the trait bound `RsaPrivateKey<16, 8>: DeterministicSigningKey<..>` is not satisfied. +//! let _ = DeterministicSigningKey::::sign_deterministic(sk, b"", None); +//! } +//! ``` + +use crate::keys::RsaPublicKey; +use crate::rsassa_pkcs1_v1_5::RSASSA_PKCS1_v1_5; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::traits::SignaturePublicKey; +use bouncycastle_sha2::{SHA256, SHA384}; + +/// An RSA-1024 public key. +pub type RSA1024PublicKey = RsaPublicKey<16>; + +/// Encoded length of an [`RSA1024PublicKey`] under [`SignaturePublicKey`]: 128 + 4 bytes (see +/// [`RsaPublicKey`]'s `# Encoding`). +pub const PK_LEN: usize = 132; +/// Signature length: `k`, the modulus length in octets (RFC 8017 §8.2.1 step 2.c). +pub const SIG_LEN: usize = 128; + +impl SignaturePublicKey for RSA1024PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + self.encode_raw::<128, PK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + *out = self.encode_raw::<128, PK_LEN>(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; PK_LEN] = bytes + .try_into() + .map_err(|_| SignatureError::DecodingError("RSA-1024 public key must be 132 bytes"))?; + Self::from_bytes_raw::<128, PK_LEN>(bytes) + } +} + +/// RSASSA-PKCS1-v1_5/SHA-256 over RSA-1024 as a `SignatureVerifier` only (see the module docs): +/// [`RSASSA_PKCS1_v1_5`] at this size's widths. The signing-side +/// widths the generic type also needs (`HALF = 8`, `HALF2 = 16`, `HALF21 = 17`, `SK_LEN = 320`) +/// are what an RSA-1024 CRT key would have if one existed; they name the type but nothing uses +/// them, since the signing-role impl they would feed does not exist for `RsaPrivateKey<16, 8>`. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA256 = + RSASSA_PKCS1_v1_5; +/// RSASSA-PKCS1-v1_5/SHA-384 over RSA-1024, verify-only; see [`RSASSA_PKCS1_v1_5_SHA256`]. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA384 = + RSASSA_PKCS1_v1_5; diff --git a/crypto/rsa/src/rsa_1536.rs b/crypto/rsa/src/rsa_1536.rs new file mode 100644 index 000000000..6771d752c --- /dev/null +++ b/crypto/rsa/src/rsa_1536.rs @@ -0,0 +1,59 @@ +//! RSA-1536: verification only. `L = 24` (1536 bits). As with [`crate::rsa_1024`], there is no +//! `RSA1536PrivateKey` and no signing function -- see that module's docs and this crate's +//! `# Scope` for why. +//! +//! Only PKCS#1 v1.5 is wired up: Wycheproof has no PSS vectors at 1536 bits (nor, realistically, +//! would PSS be paired with a modulus this small in practice -- it postdates PKCS#1 v1.5 and +//! became common alongside longer keys), so there is nothing to validate a PSS pairing against at +//! this size. +//! +//! The `SignatureVerifier` types ([`RSASSA_PKCS1_v1_5_SHA256`] and siblings) are verify-only for +//! the same reason and in the same way as [`crate::rsa_1024`]'s -- see that module's docs. + +use crate::keys::RsaPublicKey; +use crate::rsassa_pkcs1_v1_5::RSASSA_PKCS1_v1_5; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::traits::SignaturePublicKey; +use bouncycastle_sha2::{SHA256, SHA384, SHA512}; + +/// An RSA-1536 public key. +pub type RSA1536PublicKey = RsaPublicKey<24>; + +/// Encoded length of an [`RSA1536PublicKey`] under [`SignaturePublicKey`]: 192 + 4 bytes (see +/// [`RsaPublicKey`]'s `# Encoding`). +pub const PK_LEN: usize = 196; +/// Signature length: `k`, the modulus length in octets (RFC 8017 §8.2.1 step 2.c). +pub const SIG_LEN: usize = 192; + +impl SignaturePublicKey for RSA1536PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + self.encode_raw::<192, PK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + *out = self.encode_raw::<192, PK_LEN>(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; PK_LEN] = bytes + .try_into() + .map_err(|_| SignatureError::DecodingError("RSA-1536 public key must be 196 bytes"))?; + Self::from_bytes_raw::<192, PK_LEN>(bytes) + } +} + +/// RSASSA-PKCS1-v1_5/SHA-256 over RSA-1536 as a `SignatureVerifier` only -- see +/// [`crate::rsa_1024::RSASSA_PKCS1_v1_5_SHA256`] for why the signing-side widths (`HALF = 12`, +/// `SK_LEN = 480`) are named but inert. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA256 = + RSASSA_PKCS1_v1_5; +/// RSASSA-PKCS1-v1_5/SHA-384 over RSA-1536, verify-only; see [`RSASSA_PKCS1_v1_5_SHA256`]. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA384 = + RSASSA_PKCS1_v1_5; +/// RSASSA-PKCS1-v1_5/SHA-512 over RSA-1536, verify-only; see [`RSASSA_PKCS1_v1_5_SHA256`]. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA512 = + RSASSA_PKCS1_v1_5; diff --git a/crypto/rsa/src/rsa_2048.rs b/crypto/rsa/src/rsa_2048.rs new file mode 100644 index 000000000..c28844927 --- /dev/null +++ b/crypto/rsa/src/rsa_2048.rs @@ -0,0 +1,189 @@ +//! RSA-2048: the generic engine ([`crate::modexp`], [`crate::rsa_core`], [`crate::rsassa_pkcs1_v1_5`], +//! [`crate::rsassa_pss`]) wired to concrete widths -- `L = 32` (2048 bits), `HALF = 16` (1024-bit +//! CRT primes) -- so a caller using this size doesn't thread const-generic width parameters +//! through every call. One file per modulus size, matching [`crate::keys::RsaPrivateKey`]'s own +//! `` shape; `rsa_1024`/`rsa_3072`/`rsa_4096`/`rsa_8192` are the same pattern at their +//! own widths. PSS's salt length is fixed to each hash's own output length (`H_LEN`) throughout, +//! matching RFC 8017 §9.1 note 4's "typical" choice and Wycheproof's own +//! `rsa_pss_2048_sha256_mgf1_32_test.json`/`..._sha384_mgf1_48_test.json`. +//! +//! Each (scheme, hash) pairing is an algorithm marker the key role traits are implemented over +//! ([`RSASSA_PKCS1_v1_5_SHA256`], [`RSASSA_PSS_SHA256`], [`RSASSA_PSS_SHAKE128`], ...): one-shot +//! or streaming, with PSS's salt from the RNG handed to `create_signer` or fixed through `set_salt` +//! on the signer. The key types implement +//! `SignaturePrivateKey`/`SignaturePublicKey` here, at this size's [`SK_LEN`]/[`PK_LEN`]. + +use crate::keys::{RsaPrivateKey, RsaPublicKey}; +use crate::rsassa_pkcs1_v1_5::RSASSA_PKCS1_v1_5; +use crate::rsassa_pss::RSASSA_PSS; +use crate::rsassa_pss_shake::RSASSA_PSS_SHAKE; +use crate::rsasve::{RsaKEMPrivateKey, RsaKEMPublicKey}; +use bouncycastle_core::errors::{KEMError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + KEMPrivateKey, KEMPublicKey, RNG, SignaturePrivateKey, SignaturePublicKey, +}; +use bouncycastle_sha2::{SHA256, SHA384, SHA512}; +use bouncycastle_sha3::SHAKE128; +use core::num::NonZeroUsize; + +/// An RSA-2048 private key (`p`, `q` each 1024 bits). +pub type RSA2048PrivateKey = RsaPrivateKey<32, 16>; +/// An RSA-2048 public key. +pub type RSA2048PublicKey = RsaPublicKey<32>; + +/// Encoded length of an [`RSA2048PrivateKey`] under [`SignaturePrivateKey`]: `p || q || dP || dQ +/// || qInv`, five 128-byte values (see [`RsaPrivateKey`]'s `# Encoding`). +pub const SK_LEN: usize = 640; +/// Encoded length of an [`RSA2048PublicKey`] under [`SignaturePublicKey`]: `n || e`, 256 + 4 +/// bytes (see [`RsaPublicKey`]'s `# Encoding`). +pub const PK_LEN: usize = 260; +/// Signature length: `k`, the modulus length in octets (RFC 8017 §8.1.1/§8.2.1 step 2.c). +pub const SIG_LEN: usize = 256; + +/// As [`keygen`], but sourcing the candidates from the caller-provided RNG, which must offer a +/// security strength of at least 112 bits (SP 800-57 Part 1 Rev. 5, Table 2, `k = 2048`). Each +/// candidate prime gets 5 Miller-Rabin rounds: FIPS 186-5 Table B.1's row for 1024-bit `p` and `q` at an error probability of `2^-112`, RSA-2048's security strength (SP 800-57 Part 1 Rev. 5, Table 2). +pub fn keygen(rng: &mut dyn RNG) -> Result<(RSA2048PublicKey, RSA2048PrivateKey), SignatureError> { + crate::keygen::keygen_from_rng::<16, 32, 33>( + rng, + NonZeroUsize::new(5).expect("nonzero literal"), + SecurityStrength::_112bit, + ) +} + +impl SignaturePrivateKey for RSA2048PrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.encode_raw::<128, SK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + *out = self.encode_raw::<128, SK_LEN>(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; SK_LEN] = bytes + .try_into() + .map_err(|_| SignatureError::DecodingError("RSA-2048 private key must be 640 bytes"))?; + Self::from_bytes_raw::<128, SK_LEN>(bytes) + } +} + +impl SignaturePublicKey for RSA2048PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + self.encode_raw::<256, PK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + *out = self.encode_raw::<256, PK_LEN>(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; PK_LEN] = bytes + .try_into() + .map_err(|_| SignatureError::DecodingError("RSA-2048 public key must be 260 bytes"))?; + Self::from_bytes_raw::<256, PK_LEN>(bytes) + } +} + +/// RSASSA-PKCS1-v1_5/SHA-256 over RSA-2048 through the key role traits: [`RSASSA_PKCS1_v1_5`] +/// at this size's widths. Deterministic: PKCS#1 v1.5 has no randomness, so it is the +/// `DeterministicSigningKey` role and always reproduces the same signature. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA256 = + RSASSA_PKCS1_v1_5; +/// RSASSA-PKCS1-v1_5/SHA-384 over RSA-2048; see [`RSASSA_PKCS1_v1_5_SHA256`]. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA384 = + RSASSA_PKCS1_v1_5; +/// RSASSA-PKCS1-v1_5/SHA-512 over RSA-2048; see [`RSASSA_PKCS1_v1_5_SHA256`]. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA512 = + RSASSA_PKCS1_v1_5; + +/// RSASSA-PSS/SHA-256 (MGF1-SHA-256, 32-byte salt) over RSA-2048 through the key role traits: +/// [`RSASSA_PSS`] at this size's widths, drawing its salt from the RNG handed to `SigningKey::create_signer` +/// unless `set_salt` on the signer replaces it (see that type's docs). +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHA256 = + RSASSA_PSS; +/// RSASSA-PSS/SHA-384 (48-byte salt) over RSA-2048; see [`RSASSA_PSS_SHA256`]. +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHA384 = + RSASSA_PSS; +/// RSASSA-PSS/SHA-512 (64-byte salt) over RSA-2048; see [`RSASSA_PSS_SHA256`]. +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHA512 = + RSASSA_PSS; +/// RSASSA-PSS-SHAKE128 (RFC 8702 §3.2.1) over RSA-2048 through the key role traits: +/// [`RSASSA_PSS_SHAKE`] at this size's widths. +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHAKE128 = + RSASSA_PSS_SHAKE; + +/// An RSA-2048 private key-establishment key for [`RSASVE`], kept apart from the signing key type +/// [`RSA2048PrivateKey`] (see [`crate::rsasve`]'s `# Key separation`). It uses the same `SK_LEN`-byte +/// encoding. +pub type RSA2048KEMPrivateKey = RsaKEMPrivateKey<32, 16>; +/// An RSA-2048 public key-establishment key for [`RSASVE`]; see [`RSA2048KEMPrivateKey`]. It uses +/// the same `PK_LEN`-byte encoding. +pub type RSA2048KEMPublicKey = RsaKEMPublicKey<32>; + +/// [`RSASVE`] ciphertext length: `nLen`, the modulus length in octets (SP 800-56B Rev. 2 +/// §7.2.1.2's output `C`). +pub const CT_LEN: usize = 256; +/// [`RSASVE`] shared-secret length: also `nLen` (§7.2.1.2's output `Z`). This is the raw secret +/// value, not a derived key: see [`crate::rsasve`]. +pub const SS_LEN: usize = 256; + +/// RSASVE (SP 800-56B Rev. 2 §7.2.1) over RSA-2048 as a `KEMEncapsulator`/`KEMDecapsulator`: +/// [`crate::rsasve::RSASVE`] at this size's widths and its 112-bit security strength. +pub type RSASVE = crate::rsasve::RSASVE<32, 64, 65, 16, 32, 33, 256, 112>; + +impl RSASVE { + /// As [`Self::keygen`], but sourcing the candidates from the caller-provided RNG, with the + /// same RNG requirements as [`keygen`]. + pub fn keygen( + rng: &mut dyn RNG, + ) -> Result<(RSA2048KEMPublicKey, RSA2048KEMPrivateKey), KEMError> { + crate::rsasve::keygen_from(keygen(rng)) + } +} + +impl KEMPrivateKey for RSA2048KEMPrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.0.encode_raw::<128, SK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + *out = self.0.encode_raw::<128, SK_LEN>(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; SK_LEN] = bytes + .try_into() + .map_err(|_| KEMError::DecodingError("RSA-2048 private key must be 640 bytes"))?; + crate::rsasve::private_key_from_bytes::<32, 16, 128, SK_LEN>(bytes) + } +} + +impl KEMPublicKey for RSA2048KEMPublicKey { + fn encode(&self) -> [u8; PK_LEN] { + self.0.encode_raw::<256, PK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + *out = self.0.encode_raw::<256, PK_LEN>(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; PK_LEN] = bytes + .try_into() + .map_err(|_| KEMError::DecodingError("RSA-2048 public key must be 260 bytes"))?; + crate::rsasve::public_key_from_bytes::<32, 256, PK_LEN>(bytes) + } +} diff --git a/crypto/rsa/src/rsa_3072.rs b/crypto/rsa/src/rsa_3072.rs new file mode 100644 index 000000000..bbeed2317 --- /dev/null +++ b/crypto/rsa/src/rsa_3072.rs @@ -0,0 +1,174 @@ +//! RSA-3072: `L = 48` (3072 bits), `HALF = 24` (1536-bit CRT primes). See [`crate::rsa_2048`]'s +//! docs for the pattern every concrete modulus size in this crate follows. + +use crate::keys::{RsaPrivateKey, RsaPublicKey}; +use crate::rsassa_pkcs1_v1_5::RSASSA_PKCS1_v1_5; +use crate::rsassa_pss::RSASSA_PSS; +use crate::rsassa_pss_shake::RSASSA_PSS_SHAKE; +use crate::rsasve::{RsaKEMPrivateKey, RsaKEMPublicKey}; +use bouncycastle_core::errors::{KEMError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + KEMPrivateKey, KEMPublicKey, RNG, SignaturePrivateKey, SignaturePublicKey, +}; +use bouncycastle_sha2::{SHA256, SHA384, SHA512}; +use bouncycastle_sha3::SHAKE128; +use core::num::NonZeroUsize; + +/// An RSA-3072 private key (`p`, `q` each 1536 bits). +pub type RSA3072PrivateKey = RsaPrivateKey<48, 24>; +/// An RSA-3072 public key. +pub type RSA3072PublicKey = RsaPublicKey<48>; + +/// Encoded length of an [`RSA3072PrivateKey`] under [`SignaturePrivateKey`]: five 192-byte +/// values (see [`RsaPrivateKey`]'s `# Encoding`). +pub const SK_LEN: usize = 960; +/// Encoded length of an [`RSA3072PublicKey`] under [`SignaturePublicKey`]: 384 + 4 bytes (see +/// [`RsaPublicKey`]'s `# Encoding`). +pub const PK_LEN: usize = 388; +/// Signature length: `k`, the modulus length in octets (RFC 8017 §8.1.1/§8.2.1 step 2.c). +pub const SIG_LEN: usize = 384; + +/// As [`keygen`], but sourcing the candidates from the caller-provided RNG, which must offer a +/// security strength of at least 128 bits (SP 800-57 Part 1 Rev. 5, Table 2, `k = 3072`). Each +/// candidate prime gets 4 Miller-Rabin rounds: FIPS 186-5 Table B.1's row for 1536-bit `p` and `q` at an error probability of `2^-128`, RSA-3072's security strength (SP 800-57 Part 1 Rev. 5, Table 2). +pub fn keygen(rng: &mut dyn RNG) -> Result<(RSA3072PublicKey, RSA3072PrivateKey), SignatureError> { + crate::keygen::keygen_from_rng::<24, 48, 49>( + rng, + NonZeroUsize::new(4).expect("nonzero literal"), + SecurityStrength::_128bit, + ) +} + +impl SignaturePrivateKey for RSA3072PrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.encode_raw::<192, SK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + *out = self.encode_raw::<192, SK_LEN>(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; SK_LEN] = bytes + .try_into() + .map_err(|_| SignatureError::DecodingError("RSA-3072 private key must be 960 bytes"))?; + Self::from_bytes_raw::<192, SK_LEN>(bytes) + } +} + +impl SignaturePublicKey for RSA3072PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + self.encode_raw::<384, PK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + *out = self.encode_raw::<384, PK_LEN>(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; PK_LEN] = bytes + .try_into() + .map_err(|_| SignatureError::DecodingError("RSA-3072 public key must be 388 bytes"))?; + Self::from_bytes_raw::<384, PK_LEN>(bytes) + } +} + +/// RSASSA-PKCS1-v1_5/SHA-256 over RSA-3072 through the key role traits; see +/// [`crate::rsa_2048::RSASSA_PKCS1_v1_5_SHA256`] for the pattern. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA256 = + RSASSA_PKCS1_v1_5; +/// RSASSA-PKCS1-v1_5/SHA-384 over RSA-3072. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA384 = + RSASSA_PKCS1_v1_5; +/// RSASSA-PKCS1-v1_5/SHA-512 over RSA-3072. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA512 = + RSASSA_PKCS1_v1_5; + +/// RSASSA-PSS/SHA-256 over RSA-3072 through the key role traits; see +/// [`crate::rsa_2048::RSASSA_PSS_SHA256`] for the pattern (including where the salt comes from). +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHA256 = + RSASSA_PSS; +/// RSASSA-PSS/SHA-384 over RSA-3072. +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHA384 = + RSASSA_PSS; +/// RSASSA-PSS/SHA-512 over RSA-3072. +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHA512 = + RSASSA_PSS; +/// RSASSA-PSS-SHAKE128 (RFC 8702 §3.2.1) over RSA-3072 through the key role traits. +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHAKE128 = + RSASSA_PSS_SHAKE; + +/// An RSA-3072 private key-establishment key for [`RSASVE`], kept apart from the signing key type +/// [`RSA3072PrivateKey`] (see [`crate::rsasve`]'s `# Key separation`). It uses the same `SK_LEN`-byte +/// encoding. +pub type RSA3072KEMPrivateKey = RsaKEMPrivateKey<48, 24>; +/// An RSA-3072 public key-establishment key for [`RSASVE`]; see [`RSA3072KEMPrivateKey`]. It uses +/// the same `PK_LEN`-byte encoding. +pub type RSA3072KEMPublicKey = RsaKEMPublicKey<48>; + +/// [`RSASVE`] ciphertext length: `nLen`, the modulus length in octets (SP 800-56B Rev. 2 +/// §7.2.1.2's output `C`). +pub const CT_LEN: usize = 384; +/// [`RSASVE`] shared-secret length: also `nLen` (§7.2.1.2's output `Z`). This is the raw secret +/// value, not a derived key: see [`crate::rsasve`]. +pub const SS_LEN: usize = 384; + +/// RSASVE (SP 800-56B Rev. 2 §7.2.1) over RSA-3072 as a `KEMEncapsulator`/`KEMDecapsulator`: +/// [`crate::rsasve::RSASVE`] at this size's widths and its 128-bit security strength. +pub type RSASVE = crate::rsasve::RSASVE<48, 96, 97, 24, 48, 49, 384, 128>; + +impl RSASVE { + /// As [`Self::keygen`], but sourcing the candidates from the caller-provided RNG, with the + /// same RNG requirements as [`keygen`]. + pub fn keygen( + rng: &mut dyn RNG, + ) -> Result<(RSA3072KEMPublicKey, RSA3072KEMPrivateKey), KEMError> { + crate::rsasve::keygen_from(keygen(rng)) + } +} + +impl KEMPrivateKey for RSA3072KEMPrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.0.encode_raw::<192, SK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + *out = self.0.encode_raw::<192, SK_LEN>(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; SK_LEN] = bytes + .try_into() + .map_err(|_| KEMError::DecodingError("RSA-3072 private key must be 960 bytes"))?; + crate::rsasve::private_key_from_bytes::<48, 24, 192, SK_LEN>(bytes) + } +} + +impl KEMPublicKey for RSA3072KEMPublicKey { + fn encode(&self) -> [u8; PK_LEN] { + self.0.encode_raw::<384, PK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + *out = self.0.encode_raw::<384, PK_LEN>(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; PK_LEN] = bytes + .try_into() + .map_err(|_| KEMError::DecodingError("RSA-3072 public key must be 388 bytes"))?; + crate::rsasve::public_key_from_bytes::<48, 384, PK_LEN>(bytes) + } +} diff --git a/crypto/rsa/src/rsa_4096.rs b/crypto/rsa/src/rsa_4096.rs new file mode 100644 index 000000000..b631e3500 --- /dev/null +++ b/crypto/rsa/src/rsa_4096.rs @@ -0,0 +1,174 @@ +//! RSA-4096: `L = 64` (4096 bits), `HALF = 32` (2048-bit CRT primes). See [`crate::rsa_2048`]'s +//! docs for the pattern every concrete modulus size in this crate follows. + +use crate::keys::{RsaPrivateKey, RsaPublicKey}; +use crate::rsassa_pkcs1_v1_5::RSASSA_PKCS1_v1_5; +use crate::rsassa_pss::RSASSA_PSS; +use crate::rsassa_pss_shake::RSASSA_PSS_SHAKE; +use crate::rsasve::{RsaKEMPrivateKey, RsaKEMPublicKey}; +use bouncycastle_core::errors::{KEMError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + KEMPrivateKey, KEMPublicKey, RNG, SignaturePrivateKey, SignaturePublicKey, +}; +use bouncycastle_sha2::{SHA256, SHA384, SHA512}; +use bouncycastle_sha3::SHAKE256; +use core::num::NonZeroUsize; + +/// An RSA-4096 private key (`p`, `q` each 2048 bits). +pub type RSA4096PrivateKey = RsaPrivateKey<64, 32>; +/// An RSA-4096 public key. +pub type RSA4096PublicKey = RsaPublicKey<64>; + +/// Encoded length of an [`RSA4096PrivateKey`] under [`SignaturePrivateKey`]: five 256-byte +/// values (see [`RsaPrivateKey`]'s `# Encoding`). +pub const SK_LEN: usize = 1280; +/// Encoded length of an [`RSA4096PublicKey`] under [`SignaturePublicKey`]: 512 + 4 bytes (see +/// [`RsaPublicKey`]'s `# Encoding`). +pub const PK_LEN: usize = 516; +/// Signature length: `k`, the modulus length in octets (RFC 8017 §8.1.1/§8.2.1 step 2.c). +pub const SIG_LEN: usize = 512; + +/// As [`keygen`], but sourcing the candidates from the caller-provided RNG, which must offer a +/// security strength of at least 128 bits: SP 800-57 Part 1 Rev. 5, Table 2 lists 128 for `k = 3072` and 192 for `k = 7680`, so 4096 is at least 128. Each +/// candidate prime gets 4 Miller-Rabin rounds: FIPS 186-5 Table B.1's row for 2048-bit `p` and `q` at an error probability of `2^-144` (the strength Table B.1 itself pairs with this size). +pub fn keygen(rng: &mut dyn RNG) -> Result<(RSA4096PublicKey, RSA4096PrivateKey), SignatureError> { + crate::keygen::keygen_from_rng::<32, 64, 65>( + rng, + NonZeroUsize::new(4).expect("nonzero literal"), + SecurityStrength::_128bit, + ) +} + +impl SignaturePrivateKey for RSA4096PrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.encode_raw::<256, SK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + *out = self.encode_raw::<256, SK_LEN>(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; SK_LEN] = bytes.try_into().map_err(|_| { + SignatureError::DecodingError("RSA-4096 private key must be 1280 bytes") + })?; + Self::from_bytes_raw::<256, SK_LEN>(bytes) + } +} + +impl SignaturePublicKey for RSA4096PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + self.encode_raw::<512, PK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + *out = self.encode_raw::<512, PK_LEN>(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; PK_LEN] = bytes + .try_into() + .map_err(|_| SignatureError::DecodingError("RSA-4096 public key must be 516 bytes"))?; + Self::from_bytes_raw::<512, PK_LEN>(bytes) + } +} + +/// RSASSA-PKCS1-v1_5/SHA-256 over RSA-4096 through the key role traits; see +/// [`crate::rsa_2048::RSASSA_PKCS1_v1_5_SHA256`] for the pattern. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA256 = + RSASSA_PKCS1_v1_5; +/// RSASSA-PKCS1-v1_5/SHA-384 over RSA-4096. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA384 = + RSASSA_PKCS1_v1_5; +/// RSASSA-PKCS1-v1_5/SHA-512 over RSA-4096. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA512 = + RSASSA_PKCS1_v1_5; + +/// RSASSA-PSS/SHA-256 over RSA-4096 through the key role traits; see +/// [`crate::rsa_2048::RSASSA_PSS_SHA256`] for the pattern (including where the salt comes from). +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHA256 = + RSASSA_PSS; +/// RSASSA-PSS/SHA-384 over RSA-4096. +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHA384 = + RSASSA_PSS; +/// RSASSA-PSS/SHA-512 over RSA-4096. +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHA512 = + RSASSA_PSS; +/// RSASSA-PSS-SHAKE256 (RFC 8702 §3.2.1) over RSA-4096 through the key role traits. +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHAKE256 = + RSASSA_PSS_SHAKE; + +/// An RSA-4096 private key-establishment key for [`RSASVE`], kept apart from the signing key type +/// [`RSA4096PrivateKey`] (see [`crate::rsasve`]'s `# Key separation`). It uses the same `SK_LEN`-byte +/// encoding. +pub type RSA4096KEMPrivateKey = RsaKEMPrivateKey<64, 32>; +/// An RSA-4096 public key-establishment key for [`RSASVE`]; see [`RSA4096KEMPrivateKey`]. It uses +/// the same `PK_LEN`-byte encoding. +pub type RSA4096KEMPublicKey = RsaKEMPublicKey<64>; + +/// [`RSASVE`] ciphertext length: `nLen`, the modulus length in octets (SP 800-56B Rev. 2 +/// §7.2.1.2's output `C`). +pub const CT_LEN: usize = 512; +/// [`RSASVE`] shared-secret length: also `nLen` (§7.2.1.2's output `Z`). This is the raw secret +/// value, not a derived key: see [`crate::rsasve`]. +pub const SS_LEN: usize = 512; + +/// RSASVE (SP 800-56B Rev. 2 §7.2.1) over RSA-4096 as a `KEMEncapsulator`/`KEMDecapsulator`: +/// [`crate::rsasve::RSASVE`] at this size's widths and its 128-bit security strength. +pub type RSASVE = crate::rsasve::RSASVE<64, 128, 129, 32, 64, 65, 512, 128>; + +impl RSASVE { + /// As [`Self::keygen`], but sourcing the candidates from the caller-provided RNG, with the + /// same RNG requirements as [`keygen`]. + pub fn keygen( + rng: &mut dyn RNG, + ) -> Result<(RSA4096KEMPublicKey, RSA4096KEMPrivateKey), KEMError> { + crate::rsasve::keygen_from(keygen(rng)) + } +} + +impl KEMPrivateKey for RSA4096KEMPrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.0.encode_raw::<256, SK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + *out = self.0.encode_raw::<256, SK_LEN>(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; SK_LEN] = bytes + .try_into() + .map_err(|_| KEMError::DecodingError("RSA-4096 private key must be 1280 bytes"))?; + crate::rsasve::private_key_from_bytes::<64, 32, 256, SK_LEN>(bytes) + } +} + +impl KEMPublicKey for RSA4096KEMPublicKey { + fn encode(&self) -> [u8; PK_LEN] { + self.0.encode_raw::<512, PK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + *out = self.0.encode_raw::<512, PK_LEN>(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; PK_LEN] = bytes + .try_into() + .map_err(|_| KEMError::DecodingError("RSA-4096 public key must be 516 bytes"))?; + crate::rsasve::public_key_from_bytes::<64, 512, PK_LEN>(bytes) + } +} diff --git a/crypto/rsa/src/rsa_8192.rs b/crypto/rsa/src/rsa_8192.rs new file mode 100644 index 000000000..a4b97e18d --- /dev/null +++ b/crypto/rsa/src/rsa_8192.rs @@ -0,0 +1,168 @@ +//! RSA-8192: `L = 128` (8192 bits), `HALF = 64` (4096-bit CRT primes). See [`crate::rsa_2048`]'s +//! docs for the pattern every concrete modulus size in this crate follows. + +use crate::keys::{RsaPrivateKey, RsaPublicKey}; +use crate::rsassa_pkcs1_v1_5::RSASSA_PKCS1_v1_5; +use crate::rsassa_pss::RSASSA_PSS; +use crate::rsasve::{RsaKEMPrivateKey, RsaKEMPublicKey}; +use bouncycastle_core::errors::{KEMError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + KEMPrivateKey, KEMPublicKey, RNG, SignaturePrivateKey, SignaturePublicKey, +}; +use bouncycastle_sha2::{SHA256, SHA384, SHA512}; +use core::num::NonZeroUsize; + +/// An RSA-8192 private key (`p`, `q` each 4096 bits). +pub type RSA8192PrivateKey = RsaPrivateKey<128, 64>; +/// An RSA-8192 public key. +pub type RSA8192PublicKey = RsaPublicKey<128>; + +/// Encoded length of an [`RSA8192PrivateKey`] under [`SignaturePrivateKey`]: five 512-byte +/// values (see [`RsaPrivateKey`]'s `# Encoding`). +pub const SK_LEN: usize = 2560; +/// Encoded length of an [`RSA8192PublicKey`] under [`SignaturePublicKey`]: 1024 + 4 bytes (see +/// [`RsaPublicKey`]'s `# Encoding`). +pub const PK_LEN: usize = 1028; +/// Signature length: `k`, the modulus length in octets (RFC 8017 §8.1.1/§8.2.1 step 2.c). +pub const SIG_LEN: usize = 1024; + +/// As [`keygen`], but sourcing the candidates from the caller-provided RNG, which must offer a +/// security strength of at least 192 bits (SP 800-57 Part 1 Rev. 5, Table 2: `k = 7680` gives 192, and 8192 exceeds it). Each +/// candidate prime gets 4 Miller-Rabin rounds: FIPS 186-5 Table B.1 stops at 2048-bit primes; Appendix C.1's formula (2), which reproduces every entry of that table, gives 3 rounds for 4096-bit primes at `2^-192` (RSA-8192 exceeds SP 800-57's 7680-bit/192-bit row) and 5 at `2^-256`, so 4 is taken, matching the table's last row with margin. +pub fn keygen(rng: &mut dyn RNG) -> Result<(RSA8192PublicKey, RSA8192PrivateKey), SignatureError> { + crate::keygen::keygen_from_rng::<64, 128, 129>( + rng, + NonZeroUsize::new(4).expect("nonzero literal"), + SecurityStrength::_192bit, + ) +} + +impl SignaturePrivateKey for RSA8192PrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.encode_raw::<512, SK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + *out = self.encode_raw::<512, SK_LEN>(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; SK_LEN] = bytes.try_into().map_err(|_| { + SignatureError::DecodingError("RSA-8192 private key must be 2560 bytes") + })?; + Self::from_bytes_raw::<512, SK_LEN>(bytes) + } +} + +impl SignaturePublicKey for RSA8192PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + self.encode_raw::<1024, PK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + *out = self.encode_raw::<1024, PK_LEN>(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; PK_LEN] = bytes + .try_into() + .map_err(|_| SignatureError::DecodingError("RSA-8192 public key must be 1028 bytes"))?; + Self::from_bytes_raw::<1024, PK_LEN>(bytes) + } +} + +/// RSASSA-PKCS1-v1_5/SHA-256 over RSA-8192 through the key role traits; see +/// [`crate::rsa_2048::RSASSA_PKCS1_v1_5_SHA256`] for the pattern. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA256 = + RSASSA_PKCS1_v1_5; +/// RSASSA-PKCS1-v1_5/SHA-384 over RSA-8192. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA384 = + RSASSA_PKCS1_v1_5; +/// RSASSA-PKCS1-v1_5/SHA-512 over RSA-8192. +#[allow(non_camel_case_types)] +pub type RSASSA_PKCS1_v1_5_SHA512 = + RSASSA_PKCS1_v1_5; + +/// RSASSA-PSS/SHA-256 over RSA-8192 through the key role traits; see +/// [`crate::rsa_2048::RSASSA_PSS_SHA256`] for the pattern (including where the salt comes from). +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHA256 = + RSASSA_PSS; +/// RSASSA-PSS/SHA-384 over RSA-8192. +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHA384 = + RSASSA_PSS; +/// RSASSA-PSS/SHA-512 over RSA-8192. +#[allow(non_camel_case_types)] +pub type RSASSA_PSS_SHA512 = + RSASSA_PSS; + +/// An RSA-8192 private key-establishment key for [`RSASVE`], kept apart from the signing key type +/// [`RSA8192PrivateKey`] (see [`crate::rsasve`]'s `# Key separation`). It uses the same `SK_LEN`-byte +/// encoding. +pub type RSA8192KEMPrivateKey = RsaKEMPrivateKey<128, 64>; +/// An RSA-8192 public key-establishment key for [`RSASVE`]; see [`RSA8192KEMPrivateKey`]. It uses +/// the same `PK_LEN`-byte encoding. +pub type RSA8192KEMPublicKey = RsaKEMPublicKey<128>; + +/// [`RSASVE`] ciphertext length: `nLen`, the modulus length in octets (SP 800-56B Rev. 2 +/// §7.2.1.2's output `C`). +pub const CT_LEN: usize = 1024; +/// [`RSASVE`] shared-secret length: also `nLen` (§7.2.1.2's output `Z`). This is the raw secret +/// value, not a derived key: see [`crate::rsasve`]. +pub const SS_LEN: usize = 1024; + +/// RSASVE (SP 800-56B Rev. 2 §7.2.1) over RSA-8192 as a `KEMEncapsulator`/`KEMDecapsulator`: +/// [`crate::rsasve::RSASVE`] at this size's widths and its 192-bit security strength. +pub type RSASVE = crate::rsasve::RSASVE<128, 256, 257, 64, 128, 129, 1024, 192>; + +impl RSASVE { + /// As [`Self::keygen`], but sourcing the candidates from the caller-provided RNG, with the + /// same RNG requirements as [`keygen`]. + pub fn keygen( + rng: &mut dyn RNG, + ) -> Result<(RSA8192KEMPublicKey, RSA8192KEMPrivateKey), KEMError> { + crate::rsasve::keygen_from(keygen(rng)) + } +} + +impl KEMPrivateKey for RSA8192KEMPrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.0.encode_raw::<512, SK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + *out = self.0.encode_raw::<512, SK_LEN>(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; SK_LEN] = bytes + .try_into() + .map_err(|_| KEMError::DecodingError("RSA-8192 private key must be 2560 bytes"))?; + crate::rsasve::private_key_from_bytes::<128, 64, 512, SK_LEN>(bytes) + } +} + +impl KEMPublicKey for RSA8192KEMPublicKey { + fn encode(&self) -> [u8; PK_LEN] { + self.0.encode_raw::<1024, PK_LEN>() + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + *out = self.0.encode_raw::<1024, PK_LEN>(); + PK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; PK_LEN] = bytes + .try_into() + .map_err(|_| KEMError::DecodingError("RSA-8192 public key must be 1028 bytes"))?; + crate::rsasve::public_key_from_bytes::<128, 1024, PK_LEN>(bytes) + } +} diff --git a/crypto/rsa/src/rsa_core.rs b/crypto/rsa/src/rsa_core.rs new file mode 100644 index 000000000..6d7f75c84 --- /dev/null +++ b/crypto/rsa/src/rsa_core.rs @@ -0,0 +1,264 @@ +//! RSASP1 and RSAVP1 (RFC 8017 §5.2.1, §5.2.2): the raw signature and verification primitives. +//! Crate-private -- never exposed directly, only reachable through a padding scheme +//! (RSASSA-PKCS1-v1_5 or RSASSA-PSS, a later addition to this crate). RFC 8017's introduction to +//! §5 is explicit about why: "[primitives] are not intended to provide security apart from a +//! scheme" -- "textbook RSA" (signing a bare integer with no padding) is malleable and vulnerable +//! to existential forgery, so this crate does not let a caller reach [`rsasp1`]/[`rsavp1`] without +//! going through a scheme that fixes that. +//! +//! Both primitives operate on the two-prime case only (RFC 8017's `u = 2`, no additional +//! `(r_i, d_i, t_i)` triplets): [`crate::keys::RsaPrivateKey`] has no representation for +//! multi-prime RSA, so there is nothing here to extend to `u > 2`. + +use crate::keys::{RsaPrivateKey, RsaPublicKey}; +use crate::modexp::{MontgomeryContext, mod_pow, mul_mod, reduce_wide, sub_mod}; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_ec::montgomery; +use bouncycastle_ec::nat; + +/// RSAVP1 (RFC 8017 §5.2.2): `m = s^e mod n`, recovering the message representative from a +/// signature representative under the public key. +/// +/// Step 1's range check ("If the signature representative s is not between 0 and n - 1, output +/// 'signature representative out of range'") is `Err(`[`SignatureError::DecodingError`]`)` here. +pub(crate) fn rsavp1( + pk: &RsaPublicKey, + s: &[u64; L], +) -> Result<[u64; L], SignatureError> { + if nat::sub(s, pk.n()).1 != 1 { + return Err(SignatureError::DecodingError("signature representative out of range")); + } + Ok(public_exp::(pk, s)) +} + +/// `x^e mod n` under the public key, with no range check of its own: RSAVP1's step 2 (RFC 8017 +/// §5.2.2), shared with any other primitive that exponentiates by `e` -- each caller applies its +/// own specification's range check on `x` first, since those differ (RFC 8017 §5.2.2 step 1 +/// accepts "between 0 and n - 1"). +pub(crate) fn public_exp( + pk: &RsaPublicKey, + x: &[u64; L], +) -> [u64; L] { + let ctx = MontgomeryContext::::new(pk.n()) + .expect("RsaPublicKey::new already validated n is odd and nonzero"); + + let mut e_limbs = [0u64; L]; + e_limbs[0] = pk.e() as u64; + + mod_pow::(x, &e_limbs, &ctx) +} + +/// RSASP1 (RFC 8017 §5.2.1), CRT form (step 2.b, two-prime case): `s1 = m^dP mod p`, `s2 = m^dQ +/// mod q`, `h = (s1 - s2) * qInv mod p`, `s = s2 + q * h`. +/// +/// Step 1's range check ("If the message representative m is not between 0 and n - 1, output +/// 'message representative out of range'") is `Err(`[`SignatureError::DecodingError`]`)` here. +pub(crate) fn rsasp1< + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, +>( + sk: &RsaPrivateKey, + m: &[u64; L], +) -> Result<[u64; L], SignatureError> { + if nat::sub(m, sk.n()).1 != 1 { + return Err(SignatureError::DecodingError("message representative out of range")); + } + + Ok(crt_exp::(sk, m)) +} + +/// `m^d mod n` through the CRT quintuple, with no range check of its own: RSASP1's step 2.b (RFC +/// 8017 §5.2.1, two-prime case), shared with any other primitive that exponentiates by the private +/// key -- each caller applies its own specification's range check on `m` first, since those +/// differ (RFC 8017 §5.2.1 step 1 accepts "between 0 and n - 1"). `m < n` is all this function +/// itself relies on. +pub(crate) fn crt_exp< + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, +>( + sk: &RsaPrivateKey, + m: &[u64; L], +) -> [u64; L] { + let ctx_p = MontgomeryContext::::new(sk.p()) + .expect("RsaPrivateKey::from_crt_components already validated p is odd and nonzero"); + let ctx_q = MontgomeryContext::::new(sk.q()) + .expect("RsaPrivateKey::from_crt_components already validated q is odd and nonzero"); + + // RFC 8017 §5.2.1 step 2.b.1: reducing `m` (n-width) down to each prime's width is not itself + // part of the RFC's stated steps, which write "m^dP mod p" as if `m` already fit -- but `m` + // is as wide as `n = p * q`, so a real implementation must reduce it first. `p`/`q` are + // private key material, so this uses the constant-time (in the modulus) `reduce_wide`, not + // `bouncycastle_ec`-style variable-time reduction (which is only safe for RSA's *public* `n`). + let m_mod_p = reduce_wide::(m, sk.p()); + let m_mod_q = reduce_wide::(m, sk.q()); + + let s1 = mod_pow::(&m_mod_p, sk.d_p(), &ctx_p); + let s2 = mod_pow::(&m_mod_q, sk.d_q(), &ctx_q); + + // Step 2.b.3: `h = (s1 - s2) * qInv mod p`, which needs `s2` reduced mod `p` first. RFC 8017 + // does not require `p` and `q` to be close in magnitude (only that both are the modulus's two + // prime factors), so `s2 < q` gives no useful bound relative to `p` in general -- see + // `RsaPrivateKey`'s docs. `reduce_wide` (built for exactly this: reducing a value wider than + // its target modulus, constant-time in that modulus) handles the general case; it does not + // require its first width to exceed its second. + let s2_mod_p = reduce_wide::(&s2, sk.p()); + let diff = sub_mod::(&s1, &s2_mod_p, sk.p()); + let h = mul_mod::(&diff, sk.q_inv(), &ctx_p); + + // Step 2.b.4: `s = s2 + q * h`. `h < p` and `q * h < q * p = n`, and CRT recombination + // guarantees `s2 + q*h < n` (Garner's algorithm's own correctness property), so this widening + // multiply-then-add cannot overflow `L` limbs. + let q_h = montgomery::widening_mul::(sk.q(), &h); + let mut s2_wide = [0u64; L]; + s2_wide[..HALF].copy_from_slice(&s2); + let (s, carry) = nat::add(&s2_wide, &q_h); + debug_assert_eq!(carry, 0, "CRT recombination must stay within n's width"); + + s +} + +#[cfg(test)] +mod tests { + //! `rsasp1`/`rsavp1` are deliberately crate-private (see the module docs), so they cannot be + //! reached from `tests/` the way every other primitive in this crate is exercised -- this is + //! the "high-risk code that ... cannot be reached through the public API" exception + //! `CLAUDE.md`'s testing notes describe. Every value below is a genuine 2048-bit RSA keypair + //! (two independently generated, Miller-Rabin-tested ~1024-bit primes, `e = 65537`), and every + //! expected signature was cross-checked two ways in Python before being pasted here: CRT + //! recombination against plain `pow(m, d, n)`, and `pow(pow(m, d, n), e, n) == m`. + + use super::*; + + fn keypair() -> RsaPrivateKey<32, 16> { + let p: [u64; 16] = [ + 0xfec673c1b07d4997, 0x98c9e3737faecc8b, 0x01369d6bebcfd19b, 0x8aadc1799d493a6e, + 0x958ca74fe2a455a1, 0x686e62c9e375661d, 0xbf73814b7bf6fe14, 0x791ffe2d364719e4, + 0xc82e25af581825a1, 0x716d239de20732c7, 0xdb517e266b0c15bc, 0xe92c721464fa1e50, + 0x683e7fcc3898a002, 0xc7f69cb8c57e630f, 0xc467ab3a006acdf7, 0xe189725eddf8ccb3, + ]; + let q: [u64; 16] = [ + 0x7b4cf8c928cc535f, 0x59f055409b91aa89, 0xcd0ba689661c56d5, 0xe4659bfb595039f4, + 0x751842ab04e21054, 0xe2c2655760ebb5fc, 0xb2f71094e48dc76b, 0x2efed995e680919d, + 0x83c35ffcfdb9ee9d, 0xfe4a5fdb9f28a01b, 0x0ebf4a61e23c7524, 0x8cc355a69e45d7e6, + 0x9589828fca436940, 0x4cd1bfe875cddd70, 0x203e0d5e8d4d3acb, 0x86278c27281306ec, + ]; + let d_p: [u64; 16] = [ + 0xa7c48ae44e99dd99, 0xa6794425077c6c01, 0x0f7f8dfabf278380, 0xb481c3d6cf359f60, + 0xa5b5efbc5d9f744a, 0x91b95de874b741a1, 0xea3152a3da97ddf5, 0xe180bda330e9eba0, + 0xbcbfcc51e73a7a34, 0x019d0405e258c3c0, 0x0b08c52c187e3667, 0x84a723999981f7c1, + 0x5e50dc3287b126ff, 0xd0204fd4674f4ac6, 0x6e786ef11d0686f6, 0xa82c39200c1c4d67, + ]; + let d_q: [u64; 16] = [ + 0x0bc1fcecb5c853bf, 0x2000c1caf24e96d2, 0xb423419c07e0736a, 0x90bcb6fb03d68029, + 0xd811d2e24994ff2e, 0x5a89a6b7c7c83737, 0xbdea803dc4157f87, 0xffe93a09240b9b65, + 0xe6f584c8dc80c30a, 0x5e66076e22c91c3c, 0xb41221e6b67af91b, 0x2e3d8c36b1d9d161, + 0x7b1309bb3857cb1f, 0xee8de5186c920235, 0x5e4785a534bec7a3, 0x85883dd007ea907c, + ]; + let q_inv: [u64; 16] = [ + 0xf1be36246c217ae1, 0x079e038ceb7506d7, 0x2021f982bc5f6713, 0x55fa8fdd926a4d0f, + 0xb927b5edab1a24b8, 0xc2a1d37632cfb70e, 0x9ef39b4a3f144e8a, 0x70efa5974ed43d2f, + 0x6ea50741414ce321, 0x7df35889e34d4f38, 0x859106459f51850b, 0xd59e422a92f4a7a2, + 0x0c1e16e70fcbe362, 0xcf95cadb6a1f450d, 0x106a4e948e405012, 0x95a3415bfb9b5ff6, + ]; + RsaPrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("genuine CRT components must be accepted") + } + + fn public_key(sk: &RsaPrivateKey<32, 16>) -> RsaPublicKey<32> { + RsaPublicKey::new(sk.n(), 0x00010001).expect("e = 65537 is valid") + } + + #[test] + fn rsasp1_matches_python_crt_and_rsavp1_recovers_m() { + let sk = keypair(); + let pk = public_key(&sk); + + let m: [u64; 32] = [ + 0x000000e8d4a50fff, 0x0000000000000000, 0x0000000000000000, 0x7ff3af8ff7be0800, + 0x67ad7342574d2811, 0x4976e38546b81ac1, 0x2b70b32f6c55125e, 0x00cb3167f0fdb915, + 0x370a38a0a48c0523, 0x6a9a91b20989ab70, 0x612d6fd7e2bb4495, 0x0000000000015112, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, + ]; + let expected_s: [u64; 32] = [ + 0x364cc26d0358bbf4, 0xe66706a5c785ddb2, 0x988d10d740263405, 0x8730f3278e45205e, + 0xaf89fbc49c9a1249, 0x9eb8f7a84273ca48, 0xabaf5851d3f5426c, 0x0ba36a5d9c8c66a3, + 0x4ed17840c5f6e1da, 0x9e2acb65a840fecb, 0x738ba4ec9cdd22f7, 0x9f55aaf16f290bac, + 0x0882623816565d42, 0x0377a272e9477155, 0x479e6d3b5fc7cdf0, 0x982fc26936177b5a, + 0x0b5865236931b18c, 0xf013b20ea0f2a40b, 0xa30f42ffc4e00f37, 0xac385621648ddc31, + 0x1516eadafe0e2d2e, 0xa1241270647a6b51, 0x682d73ffca96c494, 0x6f2ab34339689b72, + 0x9cc67858bfcb237c, 0x3ca10b66cf56d9dc, 0x9e2aecc899fa34b9, 0x87ae32e45faca1d8, + 0xe6407b5fa454d3c9, 0x7c66144074c5739e, 0x72db535464e48005, 0x3e7bf2011ccdf96c, + ]; + + let s = rsasp1::<32, 64, 65, 16, 32, 33>(&sk, &m).expect("m is in range"); + assert_eq!(s, expected_s); + + let recovered = rsavp1::<32, 64, 65>(&pk, &s).expect("s is in range"); + assert_eq!(recovered, m); + } + + #[test] + fn rsasp1_second_vector_near_n_minus_1() { + let sk = keypair(); + let pk = public_key(&sk); + + // m2 = n - 12345, exercising a message representative close to the top of the range. + let m2: [u64; 32] = [ + 0xbc9607fe59ae13d0, 0x33fb0be666401931, 0x29fe66e068d6028b, 0xf4225b99e1483032, + 0x3e05752e447f3448, 0x3621f382f494e49c, 0xb12e1f7bd18e9f62, 0xba911df383b4764d, + 0x206002df096d6863, 0x07e881cdab73cfe3, 0x4e9ca660d04d6b6c, 0xfdd0169786ca06a2, + 0xffc09630c54f5731, 0x79ff3ac5ce73cc72, 0x72dd018024dd28a7, 0x4155291016140929, + 0x21a0943f3500f009, 0xd386df1b9afb0808, 0xd06dca00e14b1311, 0x31ed2137effa6d2a, + 0xb914de4e6361f98f, 0x181ad8797a7574fb, 0x98285be3abda6248, 0xcce5cb7a03c57a7d, + 0x3bdb7daa02c2b084, 0x8bb59db9b76bd95d, 0x95e3a77cea5ff912, 0x841d24a921abd3db, + 0x2895f5b063db401c, 0xe57dc21fc3866988, 0xb88f89afa429e300, 0x7630c947be6e9710, + ]; + let expected_s2: [u64; 32] = [ + 0x279fb473681292dc, 0x90dd3361616e1fba, 0x4be153bf0cc9bc54, 0xc182310ffe516be1, + 0xde08597f0526ba7f, 0x0ecf2548fd2becbd, 0xf100a0d8ab9cac48, 0x7e79521337c90850, + 0x642ecb9c26db1f8b, 0x888c707e26d47319, 0x3fe925626dabfde5, 0x5e374b37fa2875b2, + 0x57afec45ed3a06de, 0xa2184c3a6deb8156, 0x466017505ec5110d, 0x6b462b5d6f8c46f8, + 0x22b90fde8ff49648, 0xe6b9526894cfffba, 0xf5193a402a3c063c, 0xdc2d25c6ec27f87b, + 0x272f960bef731aed, 0xb8d7c2a5e9ef52cf, 0x5e82a247d76a2bbe, 0x018d6155cbc9c0fd, + 0x16f1861e4a32fa83, 0xab553b4f2fe5e561, 0x1c55445d8e2e8530, 0xa08e2522099e36e7, + 0xbcda0d67e80f74f5, 0xe8b549b81d5eb31d, 0xf1d74cf682145c5d, 0x74643dc08434c3e1, + ]; + + let s2 = rsasp1::<32, 64, 65, 16, 32, 33>(&sk, &m2).expect("m2 is in range"); + assert_eq!(s2, expected_s2); + assert_eq!(rsavp1::<32, 64, 65>(&pk, &s2).expect("s2 is in range"), m2); + } + + #[test] + fn rsasp1_rejects_out_of_range_message() { + let sk = keypair(); + let out_of_range = *sk.n(); + assert!(matches!( + rsasp1::<32, 64, 65, 16, 32, 33>(&sk, &out_of_range), + Err(SignatureError::DecodingError(_)) + )); + } + + #[test] + fn rsavp1_rejects_out_of_range_signature() { + let sk = keypair(); + let pk = public_key(&sk); + let out_of_range = *pk.n(); + assert!(matches!( + rsavp1::<32, 64, 65>(&pk, &out_of_range), + Err(SignatureError::DecodingError(_)) + )); + } +} diff --git a/crypto/rsa/src/rsassa_pkcs1_v1_5.rs b/crypto/rsa/src/rsassa_pkcs1_v1_5.rs new file mode 100644 index 000000000..41ec71d18 --- /dev/null +++ b/crypto/rsa/src/rsassa_pkcs1_v1_5.rs @@ -0,0 +1,473 @@ +//! RSASSA-PKCS1-v1_5 (RFC 8017 §8.2): sign and verify, built on [`crate::rsa_core`]'s +//! RSASP1/RSAVP1 and [`crate::emsa_pkcs1_v1_5`]'s deterministic encoding. Generic over every +//! width this crate's arithmetic needs (`L`/`L2`/`L21` for the modulus, `HALF`/`HALF2`/`HALF21` +//! for each CRT prime, `K_LEN = 8 * L` for the encoded message/signature byte length) and over +//! the hash function `H` (`H_LEN`/`T_LEN` sized to it) -- a concrete modulus size wires these to +//! literals (e.g. RSA-2048/SHA-256 in [`crate::rsa_2048`]) rather than a caller choosing them. +//! +//! [`RSASSA_PKCS1_v1_5`] is the algorithm marker the key role traits ([`DeterministicSigningKey`], +//! [`VerifyingKey`]) are implemented over at those widths -- one-shot and streaming, with a +//! `&[u8]`-typed signature on the verify side -- on top of [`sign_from_hash`]/[`verify_from_hash`], +//! which take the message hash rather than the message and are the pre-hash entry points for a +//! caller that has already computed it. + +use crate::codec::{be_bytes_from_limbs, limbs_from_be_bytes}; +use crate::emsa_pkcs1_v1_5::{emsa_pkcs1_v1_5_encode_from_hash, emsa_pkcs1_v1_5_verify_from_hash}; +use crate::keys::{RsaPrivateKey, RsaPublicKey, modulus_strength}; +use crate::rsa_core::{rsasp1, rsavp1}; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::signature_key::{DeterministicSigningKey, SignatureAlgorithm, VerifyingKey}; +use bouncycastle_core::traits::{ + Algorithm, AlgorithmInfo, AlgorithmOID, HashAlgParams, HashOneShot, SignaturePrivateKey, + SignaturePublicKey, SignatureSigner, SignatureVerifier, +}; +use core::marker::PhantomData; + +/// RSASSA-PKCS1-v1_5 (RFC 8017 §8.2) over `H`, at a modulus of `L` limbs: the algorithm marker the key role +/// traits are implemented over, with every width fixed by the aliases in the size modules +/// (`rsa_2048::RSASSA_PKCS1_v1_5_SHA256` and siblings). +/// `RsaPrivateKey` signs through [`DeterministicSigningKey`]: PKCS#1 v1.5 draws nothing, so the +/// same key and message always give the same signature. +/// `RsaPublicKey` verifies through [`VerifyingKey`]. One key type serves every scheme and hash, so +/// a call names the marker: `DeterministicSigningKey::::...(&sk, ..)`. `ctx` is +/// accepted and ignored, since RSA has no context string. +#[allow(non_camel_case_types)] +pub struct RSASSA_PKCS1_v1_5< + H, + const H_LEN: usize, + const T_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> { + _hash: PhantomData, +} + +impl< + H, + const H_LEN: usize, + const T_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> Algorithm + for RSASSA_PKCS1_v1_5 +{ + const ALG_NAME: &'static str = "RSASSA-PKCS1-v1_5"; + /// From the modulus width, by SP 800-57 Part 1 Rev. 5 Table 2; see [`modulus_strength`]. + const MAX_SECURITY_STRENGTH: SecurityStrength = modulus_strength(L); +} + +impl< + H, + const H_LEN: usize, + const T_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> SignatureAlgorithm + for RSASSA_PKCS1_v1_5 +{ +} + +/// The signer the key hands out; it hashes the message and finishes with [`RSASSA_PKCS1_v1_5`]'s encoding and +/// RSASP1. +pub struct RsaPkcs1v15Signer< + 'k, + H, + const H_LEN: usize, + const T_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> { + sk: &'k RsaPrivateKey, + hash: H, +} + +/// The verifier the key hands out: hashes the message and finishes with RSAVP1 and the +/// encoding's verification. +pub struct RsaPkcs1v15Verifier< + 'k, + H, + const H_LEN: usize, + const T_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> { + pk: &'k RsaPublicKey, + hash: H, +} + +impl< + H: HashOneShot + HashAlgParams + AlgorithmOID, + const H_LEN: usize, + const T_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> + DeterministicSigningKey< + RSASSA_PKCS1_v1_5, + K_LEN, + > for RsaPrivateKey +where + // Only a private key type this crate has declared signable (a `SignaturePrivateKey` impl in + // its size module) signs: the verify-only sizes, RSA-1024/1536, have none, so this impl does + // not exist for them -- compile-time enforcement of the crate's `# Scope`. + RsaPrivateKey: SignaturePrivateKey, +{ + type Signer<'k> + = RsaPkcs1v15Signer< + 'k, + H, + H_LEN, + T_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > + where + Self: 'k; + + fn create_signer_deterministic<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(RsaPkcs1v15Signer { sk: self, hash: H::default() }) + } +} + +impl< + H: HashOneShot + HashAlgParams + AlgorithmOID, + const H_LEN: usize, + const T_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> + VerifyingKey< + RSASSA_PKCS1_v1_5, + K_LEN, + > for RsaPublicKey +where + RsaPublicKey: SignaturePublicKey, +{ + type Verifier<'k> + = RsaPkcs1v15Verifier< + 'k, + H, + H_LEN, + T_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > + where + Self: 'k; + + fn create_verifier<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(RsaPkcs1v15Verifier { pk: self, hash: H::default() }) + } +} + +impl< + H: HashOneShot + HashAlgParams + AlgorithmOID, + const H_LEN: usize, + const T_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> AlgorithmInfo + for RsaPkcs1v15Signer< + '_, + H, + H_LEN, + T_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + fn alg_name(&self) -> &'static str { + as Algorithm>::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + as Algorithm>::MAX_SECURITY_STRENGTH + } +} + +impl< + H: HashOneShot + HashAlgParams + AlgorithmOID, + const H_LEN: usize, + const T_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> SignatureSigner + for RsaPkcs1v15Signer< + '_, + H, + H_LEN, + T_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; K_LEN], SignatureError> { + // The encoding's first step, `Hash(M)`, completed here over the streamed chunks. + let mut m_hash = [0u8; H_LEN]; + self.hash.do_final_out(&mut m_hash); + sign_from_hash::(self.sk, &m_hash) + } + + fn do_final_out(self, output: &mut [u8; K_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(K_LEN) + } +} + +impl< + H: HashOneShot + HashAlgParams + AlgorithmOID, + const H_LEN: usize, + const T_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> AlgorithmInfo + for RsaPkcs1v15Verifier< + '_, + H, + H_LEN, + T_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + fn alg_name(&self) -> &'static str { + as Algorithm>::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + as Algorithm>::MAX_SECURITY_STRENGTH + } +} + +impl< + H: HashOneShot + HashAlgParams + AlgorithmOID, + const H_LEN: usize, + const T_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> SignatureVerifier + for RsaPkcs1v15Verifier< + '_, + H, + H_LEN, + T_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + // RFC 8017 §8.1.2 / §8.2.2 step 1, "Length checking: If the length of the signature S is + // not k octets, output "invalid signature" and stop", reported as the length error the + // shared suite expects for a signature of the wrong size. + let sig: &[u8; K_LEN] = sig.try_into().map_err(|_| { + SignatureError::LengthError("RSA signature must be exactly the modulus length") + })?; + let mut m_hash = [0u8; H_LEN]; + self.hash.do_final_out(&mut m_hash); + // Step 2.b: RSAVP1's "signature representative out of range" is "invalid signature" here; + // one answer for every way a signature can fail. + verify_from_hash::(self.pk, &m_hash, sig) + .map_err(|_| SignatureError::SignatureVerificationFailed) + } +} + +/// RSASSA-PKCS1-V1_5-SIGN (RFC 8017 §8.2.1), `S = I2OSP(RSASP1(K, OS2IP(EM)), k)` with +/// `EM = EMSA-PKCS1-V1_5-ENCODE(M, k)`, given the message's hash `digest` (`H`'s output over `M`) +/// rather than `M` itself: the entry point for a caller that hashed the message incrementally +/// (the [`DeterministicSigningKey`] impl) or already holds the digest. Step 1's two errors ("message too long" -- +/// unreachable, since the encoding's length is fixed at compile time by `K_LEN` -- and "RSA +/// modulus too short") are folded into [`emsa_pkcs1_v1_5_encode_from_hash`]'s `debug_assert`, not +/// raised here as a runtime error, for the reason given there. The `H` bound is only what [`emsa_pkcs1_v1_5_encode_from_hash`] needs to build the +/// `DigestInfo`; no hashing happens here. +pub fn sign_from_hash< + H: HashAlgParams + AlgorithmOID, + const H_LEN: usize, + const T_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, +>( + sk: &RsaPrivateKey, + digest: &[u8; H_LEN], +) -> Result<[u8; K_LEN], SignatureError> { + let em = emsa_pkcs1_v1_5_encode_from_hash::(digest); + let m = limbs_from_be_bytes::(&em); + let s = rsasp1::(sk, &m)?; + Ok(be_bytes_from_limbs::(&s)) +} + +/// RSASSA-PKCS1-V1_5-VERIFY (RFC 8017 §8.2.2), given the message's hash `digest` rather than the +/// message (the counterpart of [`sign_from_hash`]): recovers `EM' = I2OSP(RSAVP1((n, e), +/// OS2IP(S)), k)` and checks it against `digest` via [`emsa_pkcs1_v1_5_verify_from_hash`] -- a +/// decode, not RFC 8017's own step 4 (re-encode and compare bytes to `EM'`), because a byte +/// comparison against this crate's own (always-`NULL`-including) encoding would reject an +/// otherwise-valid signature whose `DigestInfo` omits the `AlgorithmIdentifier`'s `NULL` +/// parameters -- a real-world leniency that function's docs explain. Step 1's length check (`S` +/// must be exactly `k` octets) doesn't apply here: `signature` is a fixed `K_LEN`-byte array, so a +/// wrong length is a type error at the call site; the [`VerifyingKey`] impl, which takes a +/// `&[u8]`, performs it. Step 2's "signature representative out of range" surfaces as +/// [`rsavp1`]'s own `Err`, propagated directly rather than folded into "invalid signature": a +/// caller working through CAVP/wycheproof-style vectors needs to tell "malformed input" apart from +/// "well-formed but wrong", the same distinction RFC 8017 itself draws by giving the range check +/// its own error text (the trait impl folds it, per step 2.b). +pub fn verify_from_hash< + H: AlgorithmOID, + const H_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const K_LEN: usize, +>( + pk: &RsaPublicKey, + digest: &[u8; H_LEN], + signature: &[u8; K_LEN], +) -> Result<(), SignatureError> { + let s = limbs_from_be_bytes::(signature); + let m = rsavp1::(pk, &s)?; + let em_prime = be_bytes_from_limbs::(&m); + + if emsa_pkcs1_v1_5_verify_from_hash::(digest, &em_prime) { + Ok(()) + } else { + Err(SignatureError::SignatureVerificationFailed) + } +} diff --git a/crypto/rsa/src/rsassa_pss.rs b/crypto/rsa/src/rsassa_pss.rs new file mode 100644 index 000000000..0e4b2185b --- /dev/null +++ b/crypto/rsa/src/rsassa_pss.rs @@ -0,0 +1,756 @@ +//! RSASSA-PSS (RFC 8017 §8.1): sign and verify, built on [`crate::rsa_core`]'s RSASP1/RSAVP1 and +//! [`crate::emsa_pss`]'s probabilistic encoding. Generic over the same width parameters as +//! [`crate::rsassa_pkcs1_v1_5`] plus PSS's own `S_LEN` (salt length) and `M_PRIME_LEN` +//! (`8 + H_LEN + S_LEN`); a concrete modulus size wires these to literals (RSA-2048/SHA-256 in +//! [`crate::rsa_2048`]). +//! +//! As in [`crate::rsassa_pkcs1_v1_5`], [`RSASSA_PSS`] is the algorithm marker the key role +//! traits are implemented over, on top of the `*_from_hash` functions, which take the message +//! hash rather than the message; the salt comes from the RNG handed to +//! [`SigningKey::create_signer`], and [`RsaPssSigner::set_salt`] replaces it for a known answer. + +use crate::codec::{be_bytes_from_limbs, limbs_from_be_bytes}; +use crate::emsa_pss::{emsa_pss_encode_from_hash, emsa_pss_verify_from_hash}; +use crate::keys::{RsaPrivateKey, RsaPublicKey, modulus_strength}; +use crate::rsa_core::{rsasp1, rsavp1}; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::signature_key::{SignatureAlgorithm, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{ + Algorithm, AlgorithmInfo, HashOneShot, RNG, SignaturePrivateKey, SignaturePublicKey, + SignatureSigner, SignatureVerifier, +}; +use core::marker::PhantomData; + +/// RSASSA-PSS (RFC 8017 §8.1) over `H`, with MGF1 over `H`, at a modulus of `L` limbs: the algorithm marker the key role +/// traits are implemented over, with every width fixed by the aliases in the size modules +/// (`rsa_2048::RSASSA_PSS_SHA256` and siblings). +/// `RsaPrivateKey` signs through [`SigningKey`]: a fresh `S_LEN`-byte salt from the caller's RNG +/// per signer (EMSA-PSS-ENCODE step 4), so two signatures of one message differ. +/// `RsaPublicKey` verifies through [`VerifyingKey`]. One key type serves every scheme and hash, so +/// a call names the marker: `SigningKey::::...(&sk, ..)`. `ctx` is +/// accepted and ignored, since RSA has no context string. +#[allow(non_camel_case_types)] +pub struct RSASSA_PSS< + H, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> { + _hash: PhantomData, +} + +impl< + H, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> Algorithm + for RSASSA_PSS< + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + const ALG_NAME: &'static str = "RSASSA-PSS"; + /// From the modulus width, by SP 800-57 Part 1 Rev. 5 Table 2; see [`modulus_strength`]. + const MAX_SECURITY_STRENGTH: SecurityStrength = modulus_strength(L); +} + +impl< + H, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> SignatureAlgorithm + for RSASSA_PSS< + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ +} + +/// The signer the key hands out; it hashes the message and finishes with [`RSASSA_PSS`]'s encoding and +/// RSASP1. `set_salt` replaces the drawn salt, for a known-answer test; the signature then depends on the key and message alone. +pub struct RsaPssSigner< + 'k, + H, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> { + sk: &'k RsaPrivateKey, + hash: H, + salt: [u8; S_LEN], +} + +impl< + 'k, + H: HashOneShot, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> + RsaPssSigner< + 'k, + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + /// Replaces the salt drawn at creation with `salt`, for a known-answer test against a + /// published salt; the signature then depends on the key and message alone. + pub fn set_salt(&mut self, salt: [u8; S_LEN]) { + self.salt = salt; + } +} + +/// The verifier the key hands out: hashes the message and finishes with RSAVP1 and the +/// encoding's verification. +pub struct RsaPssVerifier< + 'k, + H, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> { + pk: &'k RsaPublicKey, + hash: H, +} + +impl< + H: HashOneShot, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> + SigningKey< + RSASSA_PSS< + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + >, + K_LEN, + > for RsaPrivateKey +where + // Only a private key type this crate has declared signable (a `SignaturePrivateKey` impl in + // its size module) signs: the verify-only sizes, RSA-1024/1536, have none, so this impl does + // not exist for them -- compile-time enforcement of the crate's `# Scope`. + RsaPrivateKey: SignaturePrivateKey, +{ + type Signer<'k> + = RsaPssSigner< + 'k, + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > + where + Self: 'k; + + fn create_signer<'k>( + &'k self, + rng: &mut dyn RNG, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + // EMSA-PSS-ENCODE step 4's salt, drawn when the signer is created, as ECDSA draws `k`. + let mut salt = [0u8; S_LEN]; + rng.next_bytes_out(&mut salt).map_err(SignatureError::RNGError)?; + Ok(RsaPssSigner { sk: self, hash: H::default(), salt }) + } +} + +impl< + H: HashOneShot, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> + VerifyingKey< + RSASSA_PSS< + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + >, + K_LEN, + > for RsaPublicKey +where + RsaPublicKey: SignaturePublicKey, +{ + type Verifier<'k> + = RsaPssVerifier< + 'k, + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > + where + Self: 'k; + + fn create_verifier<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(RsaPssVerifier { pk: self, hash: H::default() }) + } +} + +impl< + H: HashOneShot, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> AlgorithmInfo + for RsaPssSigner< + '_, + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + fn alg_name(&self) -> &'static str { + as Algorithm>::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + as Algorithm>::MAX_SECURITY_STRENGTH + } +} + +impl< + H: HashOneShot, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> SignatureSigner + for RsaPssSigner< + '_, + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; K_LEN], SignatureError> { + // The encoding's first step, `Hash(M)`, completed here over the streamed chunks. + let mut m_hash = [0u8; H_LEN]; + self.hash.do_final_out(&mut m_hash); + sign_from_hash_with_salt::< + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + >(self.sk, &m_hash, &self.salt) + } + + fn do_final_out(self, output: &mut [u8; K_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(K_LEN) + } +} + +impl< + H: HashOneShot, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> AlgorithmInfo + for RsaPssVerifier< + '_, + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + fn alg_name(&self) -> &'static str { + as Algorithm>::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + as Algorithm>::MAX_SECURITY_STRENGTH + } +} + +impl< + H: HashOneShot, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> SignatureVerifier + for RsaPssVerifier< + '_, + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + // RFC 8017 §8.1.2 / §8.2.2 step 1, "Length checking: If the length of the signature S is + // not k octets, output "invalid signature" and stop", reported as the length error the + // shared suite expects for a signature of the wrong size. + let sig: &[u8; K_LEN] = sig.try_into().map_err(|_| { + SignatureError::LengthError("RSA signature must be exactly the modulus length") + })?; + let mut m_hash = [0u8; H_LEN]; + self.hash.do_final_out(&mut m_hash); + // Step 2.b: RSAVP1's "signature representative out of range" is "invalid signature" here; + // one answer for every way a signature can fail. + verify_from_hash::( + self.pk, &m_hash, sig, + ) + .map_err(|_| SignatureError::SignatureVerificationFailed) + } +} + +/// RSASSA-PSS-SIGN (RFC 8017 §8.1.1) with the salt supplied directly rather than drawn from an +/// RNG, given the message's hash `m_hash` (`H`'s output over the message) rather than the message +/// itself -- what [`RsaPssSigner`]'s `do_final` calls once it has hashed the streamed +/// message and settled the salt. EMSA-PSS is randomized only in its choice of salt (§9.1's own +/// note 5), so a fixed salt makes this deterministic and directly testable against a known salt. +pub fn sign_from_hash_with_salt< + H: HashOneShot, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, +>( + sk: &RsaPrivateKey, + m_hash: &[u8; H_LEN], + salt: &[u8; S_LEN], +) -> Result<[u8; K_LEN], SignatureError> { + let em = emsa_pss_encode_from_hash::( + m_hash, salt, + ); + let m = limbs_from_be_bytes::(&em); + let s = rsasp1::(sk, &m)?; + Ok(be_bytes_from_limbs::(&s)) +} + +/// RSASSA-PSS-SIGN (RFC 8017 §8.1.1) given the message's hash `m_hash` instead of the message: +/// draws a fresh `S_LEN`-byte salt from `rng` (EMSA-PSS-ENCODE step 4) and hands both to +/// [`sign_from_hash_with_salt`]. +pub fn sign_from_hash< + H: HashOneShot, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, +>( + sk: &RsaPrivateKey, + m_hash: &[u8; H_LEN], + rng: &mut dyn RNG, +) -> Result<[u8; K_LEN], SignatureError> { + let mut salt = [0u8; S_LEN]; + rng.next_bytes_out(&mut salt).map_err(SignatureError::RNGError)?; + sign_from_hash_with_salt::< + H, + H_LEN, + SEED_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + >(sk, m_hash, &salt) +} + +/// RSASSA-PSS-VERIFY (RFC 8017 §8.1.2) given the message's hash `m_hash` instead of the message +/// (the counterpart of [`sign_from_hash`]): recovers `EM = I2OSP(RSAVP1((n, e), OS2IP(S)), +/// emLen)` and checks it against `m_hash` via EMSA-PSS-VERIFY, which recovers the salt from `EM` +/// itself (§9.1.2) rather than needing it supplied. RSAVP1's range error propagates as its own +/// `Err`, as in [`crate::rsassa_pkcs1_v1_5::verify_from_hash`]. +pub fn verify_from_hash< + H: HashOneShot, + const H_LEN: usize, + const SEED_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const K_LEN: usize, +>( + pk: &RsaPublicKey, + m_hash: &[u8; H_LEN], + signature: &[u8; K_LEN], +) -> Result<(), SignatureError> { + let s = limbs_from_be_bytes::(signature); + let m = rsavp1::(pk, &s)?; + let em = be_bytes_from_limbs::(&m); + + if emsa_pss_verify_from_hash::( + m_hash, &em, + ) { + Ok(()) + } else { + Err(SignatureError::SignatureVerificationFailed) + } +} diff --git a/crypto/rsa/src/rsassa_pss_shake.rs b/crypto/rsa/src/rsassa_pss_shake.rs new file mode 100644 index 000000000..f8f4da7fc --- /dev/null +++ b/crypto/rsa/src/rsassa_pss_shake.rs @@ -0,0 +1,716 @@ +//! RSASSA-PSS with SHAKE128 or SHAKE256 as both the hash and the (native, non-MGF1) mask +//! generation function -- `id-RSASSA-PSS-SHAKE128`/`id-RSASSA-PSS-SHAKE256` (RFC 8702 §3.2.1). +//! Otherwise identical to [`crate::rsassa_pss`] (RFC 8017 §8.1): same RSASP1/RSAVP1 orchestration, +//! same fresh-salt-per-signature approach, just built on [`crate::emsa_pss_shake`] in place of +//! [`crate::emsa_pss`] -- and the same [`RSASSA_PSS_SHAKE`] marker over `*_from_hash` functions, +//! with `X: XOF` in place of `H: Hash` and no `SEED_LEN` (there is no MGF1 counter to size). + +use crate::codec::{be_bytes_from_limbs, limbs_from_be_bytes}; +use crate::emsa_pss_shake::{emsa_pss_encode_shake_from_hash, emsa_pss_verify_shake_from_hash}; +use crate::keys::{RsaPrivateKey, RsaPublicKey, modulus_strength}; +use crate::rsa_core::{rsasp1, rsavp1}; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::signature_key::{SignatureAlgorithm, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{ + Algorithm, AlgorithmInfo, HashOneShot, RNG, SignaturePrivateKey, SignaturePublicKey, + SignatureSigner, SignatureVerifier, XOF, +}; +use core::marker::PhantomData; + +/// RSASSA-PSS with a SHAKE XOF `X` as both hash and mask (RFC 8702 §3.2.1), at a modulus of `L` limbs: the algorithm marker the key role +/// traits are implemented over, with every width fixed by the aliases in the size modules +/// (`rsa_2048::RSASSA_PSS_SHAKE_SHA256` and siblings). +/// `RsaPrivateKey` signs through [`SigningKey`]: a fresh `S_LEN`-byte salt from the caller's RNG +/// per signer (EMSA-PSS-ENCODE step 4), so two signatures of one message differ. +/// `RsaPublicKey` verifies through [`VerifyingKey`]. One key type serves every scheme and hash, so +/// a call names the marker: `SigningKey::::...(&sk, ..)`. `ctx` is +/// accepted and ignored, since RSA has no context string. +#[allow(non_camel_case_types)] +pub struct RSASSA_PSS_SHAKE< + X, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> { + _hash: PhantomData, +} + +impl< + X, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> Algorithm + for RSASSA_PSS_SHAKE< + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + const ALG_NAME: &'static str = "RSASSA-PSS-SHAKE"; + /// From the modulus width, by SP 800-57 Part 1 Rev. 5 Table 2; see [`modulus_strength`]. + const MAX_SECURITY_STRENGTH: SecurityStrength = modulus_strength(L); +} + +impl< + X, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> SignatureAlgorithm + for RSASSA_PSS_SHAKE< + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ +} + +/// The signer the key hands out; it hashes the message and finishes with [`RSASSA_PSS_SHAKE`]'s encoding and +/// RSASP1. `set_salt` replaces the drawn salt, for a known-answer test; the signature then depends on the key and message alone. +pub struct RsaPssShakeSigner< + 'k, + X, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> { + sk: &'k RsaPrivateKey, + hash: X, + salt: [u8; S_LEN], +} + +impl< + 'k, + X: XOF + HashOneShot, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> + RsaPssShakeSigner< + 'k, + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + /// Replaces the salt drawn at creation with `salt`, for a known-answer test against a + /// published salt; the signature then depends on the key and message alone. + pub fn set_salt(&mut self, salt: [u8; S_LEN]) { + self.salt = salt; + } +} + +/// The verifier the key hands out: hashes the message and finishes with RSAVP1 and the +/// encoding's verification. +pub struct RsaPssShakeVerifier< + 'k, + X, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> { + pk: &'k RsaPublicKey, + hash: X, +} + +impl< + X: XOF + HashOneShot, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> + SigningKey< + RSASSA_PSS_SHAKE< + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + >, + K_LEN, + > for RsaPrivateKey +where + // Only a private key type this crate has declared signable (a `SignaturePrivateKey` impl in + // its size module) signs: the verify-only sizes, RSA-1024/1536, have none, so this impl does + // not exist for them -- compile-time enforcement of the crate's `# Scope`. + RsaPrivateKey: SignaturePrivateKey, +{ + type Signer<'k> + = RsaPssShakeSigner< + 'k, + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > + where + Self: 'k; + + fn create_signer<'k>( + &'k self, + rng: &mut dyn RNG, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + // EMSA-PSS-ENCODE step 4's salt, drawn when the signer is created, as ECDSA draws `k`. + let mut salt = [0u8; S_LEN]; + rng.next_bytes_out(&mut salt).map_err(SignatureError::RNGError)?; + Ok(RsaPssShakeSigner { sk: self, hash: X::default(), salt }) + } +} + +impl< + X: XOF + HashOneShot, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> + VerifyingKey< + RSASSA_PSS_SHAKE< + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + >, + K_LEN, + > for RsaPublicKey +where + RsaPublicKey: SignaturePublicKey, +{ + type Verifier<'k> + = RsaPssShakeVerifier< + 'k, + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > + where + Self: 'k; + + fn create_verifier<'k>( + &'k self, + _ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + Ok(RsaPssShakeVerifier { pk: self, hash: X::default() }) + } +} + +impl< + X: XOF + HashOneShot, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> AlgorithmInfo + for RsaPssShakeSigner< + '_, + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + fn alg_name(&self) -> &'static str { + as Algorithm>::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + as Algorithm>::MAX_SECURITY_STRENGTH + } +} + +impl< + X: XOF + HashOneShot, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> SignatureSigner + for RsaPssShakeSigner< + '_, + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; K_LEN], SignatureError> { + // The encoding's first step, `Hash(M)`, completed here over the streamed chunks. + let mut m_hash = [0u8; H_LEN]; + self.hash.do_final_out(&mut m_hash); + sign_from_hash_with_salt::< + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + >(self.sk, &m_hash, &self.salt) + } + + fn do_final_out(self, output: &mut [u8; K_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(K_LEN) + } +} + +impl< + X: XOF + HashOneShot, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> AlgorithmInfo + for RsaPssShakeVerifier< + '_, + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + fn alg_name(&self) -> &'static str { + as Algorithm>::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + as Algorithm>::MAX_SECURITY_STRENGTH + } +} + +impl< + X: XOF + HashOneShot, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, + const SK_LEN: usize, + const PK_LEN: usize, +> SignatureVerifier + for RsaPssShakeVerifier< + '_, + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + SK_LEN, + PK_LEN, + > +{ + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + // RFC 8017 §8.1.2 / §8.2.2 step 1, "Length checking: If the length of the signature S is + // not k octets, output "invalid signature" and stop", reported as the length error the + // shared suite expects for a signature of the wrong size. + let sig: &[u8; K_LEN] = sig.try_into().map_err(|_| { + SignatureError::LengthError("RSA signature must be exactly the modulus length") + })?; + let mut m_hash = [0u8; H_LEN]; + self.hash.do_final_out(&mut m_hash); + // Step 2.b: RSAVP1's "signature representative out of range" is "invalid signature" here; + // one answer for every way a signature can fail. + verify_from_hash::( + self.pk, &m_hash, sig, + ) + .map_err(|_| SignatureError::SignatureVerificationFailed) + } +} + +/// RSASSA-PSS-SIGN (RFC 8017 §8.1.1) with the salt supplied directly, given the message's hash +/// `m_hash` (`X`'s `H_LEN`-byte output over the message) rather than the message -- mirroring +/// [`crate::rsassa_pss::sign_from_hash_with_salt`], and what [`RsaPssShakeSigner`]'s `do_final` +/// calls. +pub fn sign_from_hash_with_salt< + X: XOF + HashOneShot, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, +>( + sk: &RsaPrivateKey, + m_hash: &[u8; H_LEN], + salt: &[u8; S_LEN], +) -> Result<[u8; K_LEN], SignatureError> { + let em = emsa_pss_encode_shake_from_hash::( + m_hash, salt, + ); + let m = limbs_from_be_bytes::(&em); + let s = rsasp1::(sk, &m)?; + Ok(be_bytes_from_limbs::(&s)) +} + +/// RSASSA-PSS-SIGN (RFC 8017 §8.1.1) given the message's hash `m_hash`: draws a fresh +/// `S_LEN`-byte salt from `rng` (EMSA-PSS-ENCODE step 4; RFC 8702 §3.2.1 fixes `S_LEN` at 32 bytes +/// for `id-RSASSA-PSS-SHAKE128` and 64 for `id-RSASSA-PSS-SHAKE256`) and hands both to +/// [`sign_from_hash_with_salt`]. +pub fn sign_from_hash< + X: XOF + HashOneShot, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const K_LEN: usize, +>( + sk: &RsaPrivateKey, + m_hash: &[u8; H_LEN], + rng: &mut dyn RNG, +) -> Result<[u8; K_LEN], SignatureError> { + let mut salt = [0u8; S_LEN]; + rng.next_bytes_out(&mut salt).map_err(SignatureError::RNGError)?; + sign_from_hash_with_salt::< + X, + H_LEN, + S_LEN, + M_PRIME_LEN, + DB_LEN, + L, + L2, + L21, + HALF, + HALF2, + HALF21, + K_LEN, + >(sk, m_hash, &salt) +} + +/// RSASSA-PSS-VERIFY (RFC 8017 §8.1.2) given the message's hash `m_hash`, mirroring +/// [`crate::rsassa_pss::verify_from_hash`] but built on the SHAKE-native EMSA-PSS-VERIFY, which +/// recovers the salt from `EM` itself. +pub fn verify_from_hash< + X: XOF + HashOneShot, + const H_LEN: usize, + const S_LEN: usize, + const M_PRIME_LEN: usize, + const DB_LEN: usize, + const L: usize, + const L2: usize, + const L21: usize, + const K_LEN: usize, +>( + pk: &RsaPublicKey, + m_hash: &[u8; H_LEN], + signature: &[u8; K_LEN], +) -> Result<(), SignatureError> { + let s = limbs_from_be_bytes::(signature); + let m = rsavp1::(pk, &s)?; + let em = be_bytes_from_limbs::(&m); + + if emsa_pss_verify_shake_from_hash::(m_hash, &em) { + Ok(()) + } else { + Err(SignatureError::SignatureVerificationFailed) + } +} diff --git a/crypto/rsa/src/rsasve.rs b/crypto/rsa/src/rsasve.rs new file mode 100644 index 000000000..6aa9d000e --- /dev/null +++ b/crypto/rsa/src/rsasve.rs @@ -0,0 +1,416 @@ +//! RSASVE, RSA Secret-Value Encapsulation (NIST SP 800-56B Rev. 2 §7.2.1), as a +//! [`bouncycastle_core`] KEM: [`RSASVE`] implements +//! [`KEMEncapsulator`]/[`KEMDecapsulator`], with RSASVE.GENERATE (§7.2.1.2) as `encaps` and +//! RSASVE.RECOVER (§7.2.1.3) as `decaps`, over the KEM-only key types [`RsaKEMPublicKey`] and +//! [`RsaKEMPrivateKey`]. +//! +//! This is the scheme BC FIPS Java 2.0 exposes as `FipsRSA.KTS_SVE` (JCA name `RSA-KAS-KEM`): the +//! encapsulating party draws a random `z` with `1 < z < n - 1` and sends `C = z^e mod n`, and both +//! parties end up holding `Z = I2BS(z, nLen)`. The shared secret *is* `Z`, the raw secret value, at +//! the modulus's full byte length. It is not a derived key. SP 800-56B Rev. 2 uses RSASVE only +//! inside its KAS1/KAS2 key-agreement schemes (§8.2, §8.3), which always pass `Z` through a key- +//! derivation method (§5.5, i.e. SP 800-56C) before use. Doing that is the caller's job here, +//! as with every other KEM in this workspace. See the crate docs' `# Security Considerations`. +//! +//! # Key separation +//! +//! The key types here wrap the signature schemes' [`RsaPublicKey`]/[`RsaPrivateKey`] rather than +//! reusing them, so the type system keeps key-establishment and signing keys apart. There is no +//! signing-role impl for an [`RsaKEMPrivateKey`] and no `KEMDecapsulator` impl for an +//! [`RsaPrivateKey`]. SP 800-56B Rev. 2 §6.1 item 5 is the requirement: "One key pair shall not +//! be used for different cryptographic purposes (for example, a digital-signature key pair shall +//! not be used for key establishment or vice versa)". BC FIPS Java checks the same thing at runtime with +//! `canBeUsed(ENCRYPT_OR_DECRYPT)`. Nothing stops a caller from building both kinds of key from +//! the same CRT components, and nothing can. +//! +//! # Key validation +//! +//! On top of what [`RsaPublicKey::new`]/[`RsaPrivateKey::from_crt_components`] already check, +//! the KEM key types enforce two things: +//! +//! * `65,537 <= e` (§6.2.1 item 2, "65,537 ≤ e < 2^256"; the upper bound holds by construction, +//! because `e` is a `u32`). This applies to the public key only: the CRT private key does not +//! carry `e`. +//! * `n` is exactly `64 * L` bits long, i.e. its top bit is set. §6.2.1 item 1 gives the key an +//! `nBits`, and §6.4.2.2's partial public-key validation requires "that the bit length of the +//! modulus shall be a length that is approved in this Recommendation". A 2048-bit key type holding +//! a shorter modulus would also break RSASVE.GENERATE's `nLen`, which is fixed at `8 * L` bytes +//! here. And with the top bit set, each draw of step 2's rejection loop succeeds with probability +//! above one half. +//! +//! The rest of SP 800-89 §5.3.3's plausibility tests, which §6.4.2.2 also requires, are not +//! performed (beyond `n` odd and `e` odd, which [`RsaPublicKey::new`] checks). Nothing here +//! amounts to full public-key validation, which §6.4.2.2 notes "is not specified in this +//! Recommendation". + +use crate::codec::{be_bytes_from_limbs, limbs_from_be_bytes}; +use crate::keys::{RsaPrivateKey, RsaPublicKey}; +use crate::rsa_core::{crt_exp, public_exp}; +use bouncycastle_core::errors::{KEMError, RNGError, SignatureError}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, RNG, +}; +use bouncycastle_ec::nat; +use bouncycastle_rng::DefaultRNG; +use bouncycastle_utils::secret::Secret; +use core::fmt; +use core::fmt::{Display, Formatter}; +use core::marker::PhantomData; + +/// The smallest public exponent SP 800-56B Rev. 2 §6.2.1 item 2 allows: "65,537 ≤ e < 2^256". +pub const MIN_PUBLIC_EXPONENT: u32 = 65537; + +/// How many `nLen`-byte draws RSASVE.GENERATE's step 2 makes before it gives up with +/// `GenericError`. §7.2.1.2 step 2.c just says "go to step 2a", with no limit. The limit exists so +/// that a broken RNG, e.g. one stuck on all-`0xFF` output, produces an error rather than a hang, the +/// same reason `crate::keygen` caps its candidate loops. `n`'s top bit is set (see the module docs' +/// `# Key validation`), so a working RNG's draw falls outside `(1, n - 1)` with probability below +/// one half, and 128 draws all fail with probability below `2^-128`. +pub const MAX_GENERATE_ATTEMPTS: usize = 128; + +/// Maps an error from the signature-typed key constructors and key generation this module wraps +/// into its [`KEMError`] equivalent. Every variant those can actually produce keeps its meaning. +fn kem_error(e: SignatureError) -> KEMError { + match e { + SignatureError::DecodingError(s) => KEMError::DecodingError(s), + SignatureError::RNGError(e) => KEMError::RNGError(e), + SignatureError::GenericError(s) => KEMError::GenericError(s), + _ => KEMError::GenericError("unexpected error from the RSA key layer"), + } +} + +/// Whether `n` is exactly `64 * L` bits: see the module docs' `# Key validation`. +fn has_full_bit_length(n: &[u64; L]) -> bool { + n[L - 1] >> 63 == 1 +} + +/// `1 < x < n - 1`, the range SP 800-56B Rev. 2 requires of RSAEP's plaintext (§7.1.1 input 2), +/// RSADP's ciphertext (§7.1.2.3 step 1), and RSASVE.GENERATE's `z` (§7.2.1.2 step 2.c). +/// +/// Written as two borrow-outs combined with `&` rather than as a short-circuiting comparison. In +/// RSASVE.GENERATE `x` is the secret `z`, and it should not matter to the timing which half of the +/// test a rejected candidate failed. +fn in_open_range(x: &[u64; L], n: &[u64; L]) -> bool { + // `n` is odd (every key type here checks it), so `n - 1` is `n` with its low bit cleared and + // needs no borrow propagation. + let mut n_minus_1 = *n; + n_minus_1[0] &= !1; + let mut one = [0u64; L]; + one[0] = 1; + // `x - (n - 1)` borrows exactly when `x < n - 1`; `1 - x` borrows exactly when `1 < x`. + let below_n_minus_1 = nat::sub(x, &n_minus_1).1; + let above_1 = nat::sub(&one, x).1; + (below_n_minus_1 & above_1) == 1 +} + +/// An RSA public key-establishment key: an [`RsaPublicKey`] with the extra checks listed in the +/// module docs' `# Key validation`, and a distinct type so that it cannot verify signatures (see +/// `# Key separation`). +/// +/// # Encoding +/// +/// `KEMPublicKey::encode`/`from_bytes` (implemented per size, e.g. for +/// `crate::rsa_2048::RSA2048KEMPublicKey`) use [`RsaPublicKey`]'s layout, `n || e`, unchanged. +/// Decoding applies this type's checks as well as [`RsaPublicKey`]'s. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RsaKEMPublicKey(pub(crate) RsaPublicKey); + +impl RsaKEMPublicKey { + /// Builds a public key from its modulus and exponent. Returns + /// `Err(`[`KEMError::DecodingError`]`)` in the cases [`RsaPublicKey::new`] rejects, and also + /// if `e < 65,537` or `n`'s top bit is clear (see the module docs' `# Key validation`). + pub fn new(n: &[u64; L], e: u32) -> Result { + Self::from_rsa_key(RsaPublicKey::new(n, e).map_err(kem_error)?) + } + + /// Applies this type's own checks to an already-built [`RsaPublicKey`]. Crate-private, so there + /// is no direct signature-key-to-KEM-key conversion in the public API. Key material still gets + /// in through its raw components or encoding, which the two kinds of key share. + pub(crate) fn from_rsa_key(pk: RsaPublicKey) -> Result { + if pk.e() < MIN_PUBLIC_EXPONENT { + return Err(KEMError::DecodingError( + "RSA key-establishment public exponent must be at least 65537", + )); + } + if !has_full_bit_length(pk.n()) { + return Err(KEMError::DecodingError("RSA modulus must have its top bit set")); + } + Ok(Self(pk)) + } + + /// The modulus `n`. + pub fn n(&self) -> &[u64; L] { + self.0.n() + } + + /// The public exponent `e`. + pub fn e(&self) -> u32 { + self.0.e() + } +} + +/// Defers to [`RsaPublicKey`]'s `Display`. Required by `KEMPublicKey`'s `Display` supertrait. +impl Display for RsaKEMPublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +/// An RSA private key-establishment key: an [`RsaPrivateKey`] (CRT form) with the modulus +/// bit-length check listed in the module docs' `# Key validation`, and a distinct type so that it +/// cannot sign (see `# Key separation`). +/// +/// # Encoding +/// +/// `KEMPrivateKey::encode`/`from_bytes` (implemented per size, e.g. for +/// `crate::rsa_2048::RSA2048KEMPrivateKey`) use [`RsaPrivateKey`]'s layout, +/// `p || q || dP || dQ || qInv`, unchanged. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RsaKEMPrivateKey(pub(crate) RsaPrivateKey); + +impl RsaKEMPrivateKey { + /// Builds a private key from its CRT components. Returns + /// `Err(`[`KEMError::DecodingError`]`)` in the cases [`RsaPrivateKey::from_crt_components`] + /// rejects, and also if `n = p * q`'s top bit is clear (see the module docs' + /// `# Key validation`). + pub fn from_crt_components( + p: &[u64; HALF], + q: &[u64; HALF], + d_p: &[u64; HALF], + d_q: &[u64; HALF], + q_inv: &[u64; HALF], + ) -> Result { + Self::from_rsa_key( + RsaPrivateKey::from_crt_components(p, q, d_p, d_q, q_inv).map_err(kem_error)?, + ) + } + + /// As [`RsaKEMPublicKey::from_rsa_key`], for the private half. + pub(crate) fn from_rsa_key(sk: RsaPrivateKey) -> Result { + if !has_full_bit_length(sk.n()) { + return Err(KEMError::DecodingError("RSA modulus must have its top bit set")); + } + Ok(Self(sk)) + } + + /// The modulus `n = p * q`. + pub fn n(&self) -> &[u64; L] { + self.0.n() + } +} + +/// RSASVE (SP 800-56B Rev. 2 §7.2.1) at one modulus size, as a +/// [`KEMEncapsulator`]/[`KEMDecapsulator`]. Each size module names its own instantiation (e.g. +/// `crate::rsa_2048::RSASVE`), along with that size's `keygen(rng)` as an inherent associated +/// function, which is where the KEM traits' docs put key generation. +/// +/// `L`/`L2`/`L21`/`HALF`/`HALF2`/`HALF21` are the widths the rest of this crate threads through +/// `rsa_core` (see [`crate::modexp`]). `N_BYTES = 8 * L` is `nLen`, which is both the ciphertext +/// length and the shared-secret length. `SECURITY_BITS` is the modulus's SP 800-57 Part 1 +/// security strength, the same value that size's `keygen` demands of its RNG. `encaps_rng` +/// demands it too, and the shared secret is labelled with it. +/// +/// Uninhabited: every operation is an associated function, as with the other KEMs in this +/// workspace. +#[allow(non_camel_case_types)] +pub struct RSASVE< + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const N_BYTES: usize, + const SECURITY_BITS: usize, +> { + _widths: PhantomData<[u64; L]>, +} + +impl< + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const N_BYTES: usize, + const SECURITY_BITS: usize, + const PK_LEN: usize, +> KEMEncapsulator, PK_LEN, N_BYTES, N_BYTES> + for RSASVE +where + RsaKEMPublicKey: KEMPublicKey, +{ + /// RSASVE.GENERATE, drawing `Z` from the library's default OS-backed RNG. See + /// [`Self::encaps_rng`]. + fn encaps(pk: &RsaKEMPublicKey) -> Result<(KeyMaterial, [u8; N_BYTES]), KEMError> { + Self::encaps_rng(pk, &mut DefaultRNG::default()) + } + + /// RSASVE.GENERATE((n, e)) (SP 800-56B Rev. 2 §7.2.1.2), drawing `Z` from `rng`. Returns + /// `(Z, C)`: the secret value as a [`KeyMaterial`] of type + /// [`KeyType::CryptographicRandom`] at the modulus's security strength, and the ciphertext. + /// + /// Errors: `RNGError(SecurityStrengthInsufficientForAlgorithm)` if `rng` is weaker than the + /// modulus (§5.3: an RBG generating `Z` "shall be instantiated to support a security strength + /// that is equal to or greater than the security strength associated with the RSA modulus + /// length"), + /// `RNGError` if `rng` fails, and `GenericError` if [`MAX_GENERATE_ATTEMPTS`] draws all fall + /// outside `(1, n - 1)`, which a working RNG does not do (see that constant). + // `Z`, `C`: SP 800-56B's own names (see QUALITY_AND_STYLE.md's naming exception). + #[allow(non_snake_case)] + fn encaps_rng( + pk: &RsaKEMPublicKey, + rng: &mut dyn RNG, + ) -> Result<(KeyMaterial, [u8; N_BYTES]), KEMError> { + debug_assert_eq!(N_BYTES, 8 * L, "RSASVE needs N_BYTES == 8 * L"); + if rng.security_strength() < SecurityStrength::from_bits(SECURITY_BITS) { + return Err(KEMError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)); + } + + // Step 1: "nLen = len(n)/8, the byte length of the modulus n" -- N_BYTES, since + // `RsaKEMPublicKey` guarantees `n` is exactly `8 * N_BYTES` bits. + + // `Z` and `z` are held in `Secret` so that they are scrubbed on every exit, including + // the error ones: §7.2.1.2's closing paragraph requires this routine to "destroy any + // locally stored portions of Z and z". + let mut Z = Secret::<[u8; N_BYTES]>::new(); + let mut z = Secret::<[u64; L]>::new(); + let mut attempts = 0; + loop { + if attempts == MAX_GENERATE_ATTEMPTS { + return Err(KEMError::GenericError( + "RSASVE.GENERATE: RNG output never fell within (1, n - 1)", + )); + } + attempts += 1; + // Step 2.a: "Using the RBG ..., generate Z, a byte string of nLen bytes." + rng.next_bytes_out(&mut *Z)?; + // Step 2.b: "z = BS2I(Z, nLen)." + *z = limbs_from_be_bytes::(&Z); + // Step 2.c: "If z does not satisfy 1 < z < (n – 1), then go to step 2a." + if in_open_range(&z, pk.n()) { + break; + } + } + + // Step 3.a: "c = RSAEP((n, e), z)". RSAEP's own step 1 range check (§7.1.1, "1 < m < + // (n – 1)") is the one step 2.c has just ensured, so it is not repeated: `public_exp` is + // RSAEP's step 2, `c = z^e mod n`. + let c = public_exp::(&pk.0, &z); + // Step 3.b: "C = I2BS(c, nLen)." + let C = be_bytes_from_limbs::(&c); + + // Step 4: "Output the string Z as the secret value, and the ciphertext C." + let mut ss = KeyMaterial::::from_bytes_as_type(&*Z, KeyType::CryptographicRandom)?; + do_hazardous_operations(&mut ss, |ss| { + ss.set_security_strength(SecurityStrength::from_bits(SECURITY_BITS)) + })?; + Ok((ss, C)) + } +} + +impl< + const L: usize, + const L2: usize, + const L21: usize, + const HALF: usize, + const HALF2: usize, + const HALF21: usize, + const N_BYTES: usize, + const SECURITY_BITS: usize, + const SK_LEN: usize, +> KEMDecapsulator, SK_LEN, N_BYTES, N_BYTES> + for RSASVE +where + RsaKEMPrivateKey: KEMPrivateKey, +{ + /// RSASVE.RECOVER((n, d), C) (SP 800-56B Rev. 2 §7.2.1.3), with the private key in CRT form + /// (§7.1.2.3's RSADP). Returns `Z` as a [`KeyMaterial`] of type + /// [`KeyType::CryptographicRandom`] at the modulus's security strength, matching what + /// [`KEMEncapsulator::encaps_rng`] returned to the other party. + /// + /// Errors: `LengthError` if `ct` is not `nLen` bytes long (step 2), and `DecapsulationFailed` + /// if RSADP finds the ciphertext out of range (step 3.c). §7.2.1.3 calls both "an indication + /// of a decryption error". They are kept as separate variants because every KEM in this workspace reports a + /// wrong-length ciphertext as `LengthError`, which `bouncycastle_core_test_framework`'s KEM + /// suite checks. A ciphertext that is in range but was never produced by `encaps` is *not* + /// detected. RSASVE has no redundancy to check, so such a ciphertext just recovers a different + /// `Z` (see the crate docs' `# Security Considerations`). + #[allow(non_snake_case)] + fn decaps(sk: &RsaKEMPrivateKey, ct: &[u8]) -> Result, KEMError> { + debug_assert_eq!(N_BYTES, 8 * L, "RSASVE needs N_BYTES == 8 * L"); + // Step 1: "nLen = len(n)/8" -- N_BYTES, as in `encaps_rng`. + + // Step 2: "If the length of the ciphertext C is not nLen bytes in length, output an + // indication of a decryption error". + let C: &[u8; N_BYTES] = ct + .try_into() + .map_err(|_| KEMError::LengthError("RSASVE ciphertext must be exactly nLen bytes"))?; + + // Step 3.a: "c = BS2I(C)." + let c = limbs_from_be_bytes::(C); + + // Step 3.b/3.c: "z = RSADP((n, d), c)", and "If RSADP indicates that the ciphertext is + // out of range, output an indication of a decryption error". This is RSADP's step 1 + // (§7.1.2.3, "If the ciphertext c does not satisfy 1 < c < (n – 1) ..."), checked here + // rather than inside a separate RSADP function, and `crt_exp` is its steps 2-5 (`mp = + // c^dP mod p`, `mq = c^dQ mod q`, `h = ((mp − mq) × qInv) mod p`, `m = mq + q × h`). + // `c` is the public ciphertext, so the range check need not be constant-time, although + // it is. + if !in_open_range(&c, sk.n()) { + return Err(KEMError::DecapsulationFailed); + } + let mut z = Secret::<[u64; L]>::new(); + *z = crt_exp::(&sk.0, &c); + + // Step 3.d: "Z = I2BS(z, nLen)." + let mut Z = Secret::<[u8; N_BYTES]>::new(); + *Z = be_bytes_from_limbs::(&z); + + // Step 4: "Output the string Z as the secret value (i.e., the shared secret)". + let mut ss = KeyMaterial::::from_bytes_as_type(&*Z, KeyType::CryptographicRandom)?; + do_hazardous_operations(&mut ss, |ss| { + ss.set_security_strength(SecurityStrength::from_bits(SECURITY_BITS)) + })?; + Ok(ss) + } +} + +/// Wraps a size module's signature-typed `keygen` result as a KEM key pair, applying +/// the KEM key types' checks. These always pass for this crate's own keys, which have `e = 65537` +/// and an `n` of exactly `nlen` bits by construction (see [`crate::keygen`]). +pub(crate) fn keygen_from( + pair: Result<(RsaPublicKey, RsaPrivateKey), SignatureError>, +) -> Result<(RsaKEMPublicKey, RsaKEMPrivateKey), KEMError> { + let (pk, sk) = pair.map_err(kem_error)?; + Ok((RsaKEMPublicKey::from_rsa_key(pk)?, RsaKEMPrivateKey::from_rsa_key(sk)?)) +} + +/// Decodes [`RsaPrivateKey`]'s raw layout into an [`RsaKEMPrivateKey`], for the per-size +/// `KEMPrivateKey::from_bytes` impls, which reject a wrong-length slice before calling this. +pub(crate) fn private_key_from_bytes< + const L: usize, + const HALF: usize, + const HALF_BYTES: usize, + const SK_LEN: usize, +>( + bytes: &[u8; SK_LEN], +) -> Result, KEMError> { + RsaKEMPrivateKey::from_rsa_key( + RsaPrivateKey::from_bytes_raw::(bytes).map_err(kem_error)?, + ) +} + +/// Decodes [`RsaPublicKey`]'s raw layout into an [`RsaKEMPublicKey`], for the per-size +/// `KEMPublicKey::from_bytes` impls, which reject a wrong-length slice before calling this. +pub(crate) fn public_key_from_bytes( + bytes: &[u8; PK_LEN], +) -> Result, KEMError> { + RsaKEMPublicKey::from_rsa_key( + RsaPublicKey::from_bytes_raw::(bytes).map_err(kem_error)?, + ) +} diff --git a/crypto/rsa/tests/keygen_tests.rs b/crypto/rsa/tests/keygen_tests.rs new file mode 100644 index 000000000..779c5d787 --- /dev/null +++ b/crypto/rsa/tests/keygen_tests.rs @@ -0,0 +1,317 @@ +//! Tests for [`bouncycastle_rsa::keygen`] and the per-size `keygen` functions: +//! known-answer checks of the Miller-Rabin test, and FIPS 186-5 Appendix A.1.1's criteria checked +//! on generated RSA-2048 (and, more slowly, RSA-3072) key pairs, plus the failure modes a broken +//! RNG produces. +//! +//! Key generation is randomized, so there is no fixed expected key; the generated pair is checked +//! against the criteria the generator claims to meet -- the candidates' top bits and parity (A.1.3 +//! steps 4.2.1/4.3), `e` coprime to `p - 1` and `q - 1` (A.1.1 2(a)), `|p - q| > 2^(nlen/2 - 100)` +//! (2(d)), `n = p * q`, and the primality of `p` and `q` under this crate's own Miller-Rabin, +//! whose known-answer tests are in this file too -- and by signing and verifying with it under +//! both schemes, which exercises every CRT component. The primality facts asserted below (the +//! documented Wycheproof `p`/`q` are prime, `p + 2` has the factor 173, `p + 6` is composite with +//! no factor below 2000, `2^521 - 1` and `2^607 - 1` are prime, `2^521 + 1` is divisible by 3) were +//! confirmed offline in Python before being pinned here; nothing here depends on a bignum crate. +//! +//! The RSA-4096 and RSA-8192 generators are exercised only under `--ignored`: an RSA-8192 +//! candidate's Miller-Rabin round is a 4096-bit modular exponentiation and a key needs hundreds of +//! candidates, which is tens of minutes in a debug build (the two together take about half a +//! minute in release). Run them with +//! `cargo test -p bouncycastle-rsa --release --test keygen_tests -- --ignored`. + +use bouncycastle_core::errors::{RNGError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::RNG; +use bouncycastle_core::traits::SignaturePrivateKey; +use bouncycastle_core_test_framework::signature::TestFrameworkSignature; +use bouncycastle_ec::montgomery::widening_mul; +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::keygen::{PUBLIC_EXPONENT, is_probable_prime}; +use bouncycastle_rsa::rsa_2048::{ + self, RSA2048PrivateKey, RSA2048PublicKey, RSASSA_PKCS1_v1_5_SHA256, RSASSA_PSS_SHA256, SIG_LEN, +}; +use bouncycastle_rsa::{rsa_3072, rsa_4096, rsa_8192}; +use core::num::NonZeroUsize; + +/// The documented Wycheproof RSA-2048 key's `p` and `q` (see `rsa_2048_pkcs1_v1_5_tests.rs` for +/// their provenance): two genuine 1024-bit primes. +const P: [u64; 16] = [ + 0x0ea36cfb3a5b18f1, 0x48a6e65332119129, 0x110ad9e7b48a1c93, 0x569156b90113e2e9, + 0xe79813a575cfad9c, 0x69d659d143ec6f17, 0xe81e6bab5ddaa783, 0xbff1c5b80a69f788, + 0x978f6c35814f50ee, 0xe6a289ad4cfbf78f, 0x34d5681e5809d415, 0xbb028bda42eeb5d2, + 0x41c56e4de086b0d5, 0x58b8d1e24f3b55d0, 0xfb5248247d98cb7d, 0xdc431050f782e894, +]; +const Q: [u64; 16] = [ + 0x669f140cfbc20f25, 0xb97bb03677207d95, 0xfd4e06f3ed7299d4, 0x160f90536abc9492, + 0xf5b131f39098f7bc, 0xae8d72c57088d7ab, 0x89b94fbde542aba9, 0x3d3f9880ec47d5e0, + 0x1378a6868af3b7a0, 0x5544070beb057c94, 0x16611debc472fac4, 0xe500ffb79f5b8868, + 0x308a5e32196603b2, 0xea5fb19eb4eabc38, 0x122273ae3222b598, 0xbd1a81e7977f9898, +]; + +fn small(v: u64) -> [u64; N] { + let mut out = [0u64; N]; + out[0] = v; + out +} + +/// `P + k` for a small `k` (no carry out of the low limb for the values used here). +fn p_plus(k: u64) -> [u64; 16] { + let mut out = P; + out[0] = out[0].checked_add(k).unwrap(); + out +} + +/// `2^bits - 1` as 16 limbs. +fn mersenne(bits: u32) -> [u64; 16] { + let mut out = [0u64; 16]; + for (i, limb) in out.iter_mut().enumerate() { + let lo = 64 * i as u32; + *limb = if bits >= lo + 64 { + u64::MAX + } else if bits > lo { + (1u64 << (bits - lo)) - 1 + } else { + 0 + }; + } + out +} + +fn rounds(n: usize) -> NonZeroUsize { + NonZeroUsize::new(n).unwrap() +} + +fn mr16(w: &[u64; 16], n: usize) -> bool { + is_probable_prime::<16, 32, 33>(w, rounds(n), &mut DefaultRNG::default()).unwrap() +} + +#[test] +fn miller_rabin_accepts_known_primes() { + assert!(mr16(&P, 5)); + assert!(mr16(&Q, 5)); + assert!(mr16(&mersenne(521), 5), "2^521 - 1 (M521) is prime"); + assert!(mr16(&mersenne(607), 5), "2^607 - 1 (M607) is prime"); +} + +/// Twenty rounds here: a composite passes one random round with probability at most 1/4, so a +/// false "prime" is a `2^-40` event rather than the `2^-10` five rounds would leave. +#[test] +fn miller_rabin_rejects_known_composites() { + assert!(!mr16(&p_plus(2), 20), "p + 2 has the factor 173 (trial division)"); + assert!(!mr16(&p_plus(4), 20), "p + 4 is divisible by 3"); + assert!(!mr16(&p_plus(6), 20), "p + 6 is composite with no factor below 2000 (Miller-Rabin)"); + let mut m521_plus_2 = mersenne(521); + m521_plus_2[0] = m521_plus_2[0].wrapping_add(2); // 2^521 + 1, divisible by 3 + assert!(!mr16(&m521_plus_2, 20)); + let mut even = P; + even[0] &= !1; + assert!(!mr16(&even, 20), "even"); + assert!(!mr16(&small(0), 20)); + assert!(!mr16(&small(1), 20)); +} + +/// Below `2^20` the trial division against the primes below `2^10` is conclusive on its own +/// (Appendix B.7), so these do not touch the RNG-driven Miller-Rabin rounds at all. +#[test] +fn miller_rabin_decides_small_values_by_trial_division() { + for prime in [2u64, 3, 5, 7, 1021, 65537, 1_048_573] { + assert!(mr16(&small(prime), 1), "{prime} is prime"); + } + for composite in [4u64, 9, 15, 561, 1023, 65535, 1_048_575] { + assert!(!mr16(&small(composite), 1), "{composite} is composite"); + } +} + +/// The CRT components of an encoded private key (`p || q || dP || dQ || qInv`, each `8 * HALF` +/// big-endian bytes) as little-endian limbs. +fn crt_components(sk_bytes: &[u8]) -> [[u64; HALF]; 5] { + let field_len = 8 * HALF; + let mut out = [[0u64; HALF]; 5]; + for (f, field) in out.iter_mut().enumerate() { + let bytes = &sk_bytes[f * field_len..(f + 1) * field_len]; + for i in 0..HALF { + let start = field_len - 8 * (i + 1); + field[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + } + out +} + +fn mod_small(a: &[u64; N], m: u64) -> u64 { + let mut rem: u128 = 0; + for limb in a.iter().rev() { + rem = ((rem << 64) | u128::from(*limb)) % u128::from(m); + } + rem as u64 +} + +/// FIPS 186-5 Appendix A.1.1's criteria, checked on the CRT components of a generated key. +fn assert_fips_186_5_criteria( + sk_bytes: &[u8], + n: &[u64; HALF2], + mr_rounds: usize, +) { + let [p, q, d_p, d_q, q_inv] = crt_components::(sk_bytes); + let e = u64::from(PUBLIC_EXPONENT); + let mut rng = DefaultRNG::default(); + for (name, prime) in [("p", &p), ("q", &q)] { + // A.1.3 step 4.2.1 (two most significant bits set) and 4.3 (odd). + assert_eq!(prime[HALF - 1] >> 62, 0b11, "{name}: top two bits set"); + assert_eq!(prime[0] & 1, 1, "{name}: odd"); + // A.1.1 2(a): GCD(p - 1, e) = 1, i.e. p mod e != 1 for prime e. + assert_ne!(mod_small(prime, e), 1, "{name} - 1 must be coprime to e"); + // 2(b)/(c): probably prime. + assert!( + is_probable_prime::(prime, rounds(mr_rounds), &mut rng).unwrap(), + "{name} must be prime" + ); + } + assert_ne!(p, q); + // 2(d): |p - q| > 2^(nlen/2 - 100). nlen/2 - 100 = 64 * (HALF - 2) + 28, so the bits at and + // above that position are limb HALF - 1 and the top 36 bits of limb HALF - 2; some must be set + // (the "exactly 2^(nlen/2 - 100)" edge is a 2^-924 event, not worth a test branch). + let (diff, borrow) = bouncycastle_ec::nat::sub(&p, &q); + let diff = if borrow == 1 { bouncycastle_ec::nat::sub(&q, &p).0 } else { diff }; + assert!( + diff[HALF - 1] != 0 || diff[HALF - 2] >> 28 != 0, + "|p - q| must exceed 2^(nlen/2 - 100)" + ); + // n = p * q. + assert_eq!(widening_mul::(&p, &q), *n); + // The CRT exponents are reduced (RFC 8017 §3.2) and nonzero. + assert!(bouncycastle_ec::nat::sub(&d_p, &p).1 == 1 && d_p != [0u64; HALF]); + assert!(bouncycastle_ec::nat::sub(&d_q, &q).1 == 1 && d_q != [0u64; HALF]); + assert!(bouncycastle_ec::nat::sub(&q_inv, &p).1 == 1 && q_inv != [0u64; HALF]); +} + +#[test] +fn keygen_2048_meets_fips_186_5_criteria_and_signs() { + let (pk, sk) = rsa_2048::keygen(&mut DefaultRNG::default()).unwrap(); + assert_eq!(pk.e(), PUBLIC_EXPONENT); + assert_eq!(pk.n(), sk.n()); + assert_fips_186_5_criteria::<16, 32, 33>(&sk.encode(), pk.n(), 5); + + // Every CRT component takes part in signing, so a round trip under both schemes is a + // functional check of dP, dQ and qInv against n. + let msg = b"signed with a freshly generated RSA-2048 key"; + let sig = DeterministicSigningKey::::sign_deterministic( + &sk, msg, None, + ) + .unwrap(); + VerifyingKey::::verify(&pk, msg, None, &sig).unwrap(); + assert!( + VerifyingKey::::verify(&pk, b"other", None, &sig).is_err() + ); + let sig = + SigningKey::::sign(&sk, &mut DefaultRNG::default(), msg, None) + .unwrap(); + VerifyingKey::::verify(&pk, msg, None, &sig).unwrap(); + + // Two generations give two keys. + let (pk2, _) = rsa_2048::keygen(&mut DefaultRNG::default()).unwrap(); + assert_ne!(pk2.n(), pk.n()); +} + +/// The shared conformance suite with the real generator in the `keygen` slot, as the ECDSA tests +/// pass theirs (the fixed-key runs elsewhere in this crate exist because keygen used not to). +#[test] +fn conformance_suite_with_generated_keys() { + TestFrameworkSignature::new(false).test_deterministic_signature::(|| rsa_2048::keygen(&mut DefaultRNG::default()), false); +} + +#[test] +fn keygen_3072_meets_fips_186_5_criteria_and_signs() { + let (pk, sk) = rsa_3072::keygen(&mut DefaultRNG::default()).unwrap(); + assert_fips_186_5_criteria::<24, 48, 49>(&sk.encode(), pk.n(), 4); + let msg = b"signed with a freshly generated RSA-3072 key"; + let sig = + DeterministicSigningKey::::sign_deterministic( + &sk, msg, None, + ) + .unwrap(); + VerifyingKey::::verify(&pk, msg, None, &sig).unwrap(); +} + +#[test] +#[ignore = "tens of minutes in a debug build: run with --release --ignored"] +fn keygen_4096_meets_fips_186_5_criteria_and_signs() { + let (pk, sk) = rsa_4096::keygen(&mut DefaultRNG::default()).unwrap(); + assert_fips_186_5_criteria::<32, 64, 65>(&sk.encode(), pk.n(), 4); + let msg = b"signed with a freshly generated RSA-4096 key"; + let sig = SigningKey::::sign( + &sk, + &mut DefaultRNG::default(), + msg, + None, + ) + .unwrap(); + VerifyingKey::::verify(&pk, msg, None, &sig).unwrap(); +} + +#[test] +#[ignore = "tens of minutes in a debug build: run with --release --ignored"] +fn keygen_8192_meets_fips_186_5_criteria_and_signs() { + let (pk, sk) = rsa_8192::keygen(&mut DefaultRNG::default()).unwrap(); + assert_fips_186_5_criteria::<64, 128, 129>(&sk.encode(), pk.n(), 4); + let msg = b"signed with a freshly generated RSA-8192 key"; + let sig = + DeterministicSigningKey::::sign_deterministic( + &sk, msg, None, + ) + .unwrap(); + VerifyingKey::::verify(&pk, msg, None, &sig).unwrap(); +} + +/// An RNG whose every output is the byte `fill`, reporting `strength`. +struct ConstantRng { + fill: u8, + strength: SecurityStrength, +} +impl RNG for ConstantRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(u32::from_ne_bytes([self.fill; 4])) + } + fn next_bytes(&mut self, len: usize) -> Result, RNGError> { + Ok(vec![self.fill; len]) + } + fn next_bytes_out(&mut self, out: &mut [u8]) -> Result { + out.fill(self.fill); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> SecurityStrength { + self.strength + } +} + +/// A.1.3's preamble: the DRBG must offer the modulus's security strength (SP 800-57 Part 1: +/// 112 bits for RSA-2048). +#[test] +fn keygen_rejects_an_rng_below_the_required_strength() { + let mut weak = ConstantRng { fill: 0x42, strength: SecurityStrength::None }; + assert!(matches!( + rsa_2048::keygen(&mut weak), + Err(SignatureError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)) + )); +} + +/// Steps 4.7/5.8: an RNG that keeps producing the same candidate cannot make progress -- the one +/// candidate is either composite, or equal to `p` and so rejected as too close (5.5) -- and the +/// candidate limits (`5 * nlen`, then `10 * nlen`) turn that into an error instead of a hang. +#[test] +fn keygen_gives_up_on_a_stuck_rng() { + let mut stuck = ConstantRng { fill: 0x42, strength: SecurityStrength::_256bit }; + assert!(matches!(rsa_2048::keygen(&mut stuck), Err(SignatureError::GenericError(_)))); +} diff --git a/crypto/rsa/tests/keys_tests.rs b/crypto/rsa/tests/keys_tests.rs new file mode 100644 index 000000000..dbbd123ea --- /dev/null +++ b/crypto/rsa/tests/keys_tests.rs @@ -0,0 +1,247 @@ +//! Validation tests for [`bouncycastle_rsa::keys`]'s public API, including the +//! `SignaturePrivateKey`/`SignaturePublicKey` impls `rsa_2048` gives its aliases of these types +//! (`RsaPrivateKey<32, 16>` *is* `RSA2048PrivateKey`, so the traits' methods resolve here once +//! imported). The generic boundary conditions every size shares are covered by +//! `core-test-framework`'s `test_keys` in each size's own test file; these pin the layout itself. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_rsa::keys::{RsaPrivateKey, RsaPublicKey}; + +const P: [u64; 16] = [ + 0xfec673c1b07d4997, 0x98c9e3737faecc8b, 0x01369d6bebcfd19b, 0x8aadc1799d493a6e, + 0x958ca74fe2a455a1, 0x686e62c9e375661d, 0xbf73814b7bf6fe14, 0x791ffe2d364719e4, + 0xc82e25af581825a1, 0x716d239de20732c7, 0xdb517e266b0c15bc, 0xe92c721464fa1e50, + 0x683e7fcc3898a002, 0xc7f69cb8c57e630f, 0xc467ab3a006acdf7, 0xe189725eddf8ccb3, +]; +const Q: [u64; 16] = [ + 0x7b4cf8c928cc535f, 0x59f055409b91aa89, 0xcd0ba689661c56d5, 0xe4659bfb595039f4, + 0x751842ab04e21054, 0xe2c2655760ebb5fc, 0xb2f71094e48dc76b, 0x2efed995e680919d, + 0x83c35ffcfdb9ee9d, 0xfe4a5fdb9f28a01b, 0x0ebf4a61e23c7524, 0x8cc355a69e45d7e6, + 0x9589828fca436940, 0x4cd1bfe875cddd70, 0x203e0d5e8d4d3acb, 0x86278c27281306ec, +]; +const D_P: [u64; 16] = [ + 0xa7c48ae44e99dd99, 0xa6794425077c6c01, 0x0f7f8dfabf278380, 0xb481c3d6cf359f60, + 0xa5b5efbc5d9f744a, 0x91b95de874b741a1, 0xea3152a3da97ddf5, 0xe180bda330e9eba0, + 0xbcbfcc51e73a7a34, 0x019d0405e258c3c0, 0x0b08c52c187e3667, 0x84a723999981f7c1, + 0x5e50dc3287b126ff, 0xd0204fd4674f4ac6, 0x6e786ef11d0686f6, 0xa82c39200c1c4d67, +]; +const D_Q: [u64; 16] = [ + 0x0bc1fcecb5c853bf, 0x2000c1caf24e96d2, 0xb423419c07e0736a, 0x90bcb6fb03d68029, + 0xd811d2e24994ff2e, 0x5a89a6b7c7c83737, 0xbdea803dc4157f87, 0xffe93a09240b9b65, + 0xe6f584c8dc80c30a, 0x5e66076e22c91c3c, 0xb41221e6b67af91b, 0x2e3d8c36b1d9d161, + 0x7b1309bb3857cb1f, 0xee8de5186c920235, 0x5e4785a534bec7a3, 0x85883dd007ea907c, +]; +const Q_INV: [u64; 16] = [ + 0xf1be36246c217ae1, 0x079e038ceb7506d7, 0x2021f982bc5f6713, 0x55fa8fdd926a4d0f, + 0xb927b5edab1a24b8, 0xc2a1d37632cfb70e, 0x9ef39b4a3f144e8a, 0x70efa5974ed43d2f, + 0x6ea50741414ce321, 0x7df35889e34d4f38, 0x859106459f51850b, 0xd59e422a92f4a7a2, + 0x0c1e16e70fcbe362, 0xcf95cadb6a1f450d, 0x106a4e948e405012, 0x95a3415bfb9b5ff6, +]; +const N: [u64; 32] = [ + 0xbc9607fe59ae4409, 0x33fb0be666401931, 0x29fe66e068d6028b, 0xf4225b99e1483032, + 0x3e05752e447f3448, 0x3621f382f494e49c, 0xb12e1f7bd18e9f62, 0xba911df383b4764d, + 0x206002df096d6863, 0x07e881cdab73cfe3, 0x4e9ca660d04d6b6c, 0xfdd0169786ca06a2, + 0xffc09630c54f5731, 0x79ff3ac5ce73cc72, 0x72dd018024dd28a7, 0x4155291016140929, + 0x21a0943f3500f009, 0xd386df1b9afb0808, 0xd06dca00e14b1311, 0x31ed2137effa6d2a, + 0xb914de4e6361f98f, 0x181ad8797a7574fb, 0x98285be3abda6248, 0xcce5cb7a03c57a7d, + 0x3bdb7daa02c2b084, 0x8bb59db9b76bd95d, 0x95e3a77cea5ff912, 0x841d24a921abd3db, + 0x2895f5b063db401c, 0xe57dc21fc3866988, 0xb88f89afa429e300, 0x7630c947be6e9710, +]; + +#[test] +fn from_crt_components_accepts_a_genuine_key_and_derives_n() { + let sk = RsaPrivateKey::<32, 16>::from_crt_components(&P, &Q, &D_P, &D_Q, &Q_INV) + .expect("genuine RSA-2048 CRT components must be accepted"); + assert_eq!(*sk.n(), N); +} + +#[test] +fn from_crt_components_rejects_mismatched_widths() { + // L = 31 != 2 * 16. + assert!(matches!( + RsaPrivateKey::<31, 16>::from_crt_components(&P, &Q, &D_P, &D_Q, &Q_INV), + Err(SignatureError::DecodingError(_)) + )); +} + +#[test] +fn from_crt_components_rejects_equal_primes() { + assert!(matches!( + RsaPrivateKey::<32, 16>::from_crt_components(&P, &P, &D_P, &D_Q, &Q_INV), + Err(SignatureError::DecodingError(_)) + )); +} + +#[test] +fn from_crt_components_rejects_even_prime() { + let mut even_p = P; + even_p[0] &= !1; + assert!(matches!( + RsaPrivateKey::<32, 16>::from_crt_components(&even_p, &Q, &D_P, &D_Q, &Q_INV), + Err(SignatureError::DecodingError(_)) + )); +} + +#[test] +fn from_crt_components_rejects_even_q() { + let mut even_q = Q; + even_q[0] &= !1; + assert!(matches!( + RsaPrivateKey::<32, 16>::from_crt_components(&P, &even_q, &D_P, &D_Q, &Q_INV), + Err(SignatureError::DecodingError(_)) + )); +} + +#[test] +fn from_crt_components_rejects_dp_out_of_range() { + assert!(matches!( + RsaPrivateKey::<32, 16>::from_crt_components(&P, &Q, &P, &D_Q, &Q_INV), + Err(SignatureError::DecodingError(_)) + )); +} + +#[test] +fn from_crt_components_rejects_qinv_out_of_range() { + assert!(matches!( + RsaPrivateKey::<32, 16>::from_crt_components(&P, &Q, &D_P, &D_Q, &P), + Err(SignatureError::DecodingError(_)) + )); +} + +#[test] +fn public_key_accepts_e_65537() { + let pk = RsaPublicKey::<32>::new(&N, 0x00010001).expect("e = 65537 is valid"); + assert_eq!(*pk.n(), N); + assert_eq!(pk.e(), 0x00010001); +} + +#[test] +fn public_key_rejects_even_exponent() { + assert!(matches!(RsaPublicKey::<32>::new(&N, 65536), Err(SignatureError::DecodingError(_)))); +} + +#[test] +fn public_key_rejects_exponent_below_three() { + assert!(matches!(RsaPublicKey::<32>::new(&N, 1), Err(SignatureError::DecodingError(_)))); +} + +#[test] +fn public_key_rejects_even_modulus() { + let mut even_n = N; + even_n[0] &= !1; + assert!(matches!(RsaPublicKey::<32>::new(&even_n, 3), Err(SignatureError::DecodingError(_)))); +} + +#[test] +fn public_key_encode_round_trips() { + let pk = RsaPublicKey::<32>::new(&N, 0x10001).unwrap(); + let bytes: [u8; 260] = pk.encode(); + let decoded = RsaPublicKey::<32>::from_bytes(&bytes).expect("must decode"); + assert_eq!(decoded, pk); +} + +/// `SignaturePublicKey`'s `n || e` layout, checked field by field rather than only by round +/// trip: `n` big-endian (its most significant limb's top byte first), then `e` big-endian. +#[test] +fn public_key_encode_layout_is_n_then_e_big_endian() { + let pk = RsaPublicKey::<32>::new(&N, 0x10001).unwrap(); + let bytes = pk.encode(); + assert_eq!(bytes[..8], N[31].to_be_bytes()); + assert_eq!(bytes[248..256], N[0].to_be_bytes()); + assert_eq!(bytes[256..], [0x00, 0x01, 0x00, 0x01]); + + let mut out = [0xaau8; 260]; + assert_eq!(pk.encode_out(&mut out), 260); + assert_eq!(out, bytes); +} + +#[test] +fn public_key_from_bytes_rejects_even_modulus() { + let pk = RsaPublicKey::<32>::new(&N, 0x10001).unwrap(); + let mut bytes: [u8; 260] = pk.encode(); + bytes[255] &= !1; // clear n's low bit (last byte, big-endian) + assert!(matches!( + RsaPublicKey::<32>::from_bytes(&bytes), + Err(SignatureError::DecodingError(_)) + )); +} + +#[test] +fn public_key_from_bytes_rejects_wrong_lengths() { + let pk = RsaPublicKey::<32>::new(&N, 0x10001).unwrap(); + let bytes = pk.encode(); + assert!(matches!( + RsaPublicKey::<32>::from_bytes(&bytes[..259]), + Err(SignatureError::DecodingError(_)) + )); + let mut too_long = bytes.to_vec(); + too_long.push(0); + assert!(matches!( + RsaPublicKey::<32>::from_bytes(&too_long), + Err(SignatureError::DecodingError(_)) + )); +} + +/// `Display` (a `SignaturePublicKey` supertrait) prints `n` as one big-endian hex string and `e` +/// in hex, so a printed key can be matched by eye against a hex dump of its encoding. +#[test] +fn public_key_display_prints_big_endian_hex() { + let pk = RsaPublicKey::<32>::new(&N, 0x10001).unwrap(); + let shown = format!("{pk}"); + let n_hex: String = pk.encode()[..256].iter().map(|b| format!("{b:02x}")).collect(); + assert_eq!(shown, format!("RsaPublicKey<32> {{ n: {n_hex}, e: 10001 }}")); +} + +#[test] +fn private_key_encode_round_trips() { + let sk = RsaPrivateKey::<32, 16>::from_crt_components(&P, &Q, &D_P, &D_Q, &Q_INV).unwrap(); + let bytes: [u8; 640] = sk.encode(); + let decoded = RsaPrivateKey::<32, 16>::from_bytes(&bytes).expect("must decode"); + assert_eq!(decoded, sk); +} + +/// `SignaturePrivateKey`'s `p || q || dP || dQ || qInv` layout, each field 128 bytes big-endian. +#[test] +fn private_key_encode_layout_is_five_big_endian_fields() { + let sk = RsaPrivateKey::<32, 16>::from_crt_components(&P, &Q, &D_P, &D_Q, &Q_INV).unwrap(); + let bytes = sk.encode(); + for (i, field) in [P, Q, D_P, D_Q, Q_INV].iter().enumerate() { + let start = 128 * i; + assert_eq!(bytes[start..start + 8], field[15].to_be_bytes(), "field {i} MSB"); + assert_eq!(bytes[start + 120..start + 128], field[0].to_be_bytes(), "field {i} LSB"); + } + + let mut out = [0xaau8; 640]; + assert_eq!(sk.encode_out(&mut out), 640); + assert_eq!(out, bytes); +} + +#[test] +fn private_key_from_bytes_rejects_equal_primes() { + let sk = RsaPrivateKey::<32, 16>::from_crt_components(&P, &Q, &D_P, &D_Q, &Q_INV).unwrap(); + let mut bytes: [u8; 640] = sk.encode(); + // Overwrite q (the second 128-byte field) with p, making the two primes equal. + let p_field = bytes[..128].to_vec(); + bytes[128..256].copy_from_slice(&p_field); + assert!(matches!( + RsaPrivateKey::<32, 16>::from_bytes(&bytes), + Err(SignatureError::DecodingError(_)) + )); +} + +#[test] +fn private_key_from_bytes_rejects_wrong_lengths() { + let sk = RsaPrivateKey::<32, 16>::from_crt_components(&P, &Q, &D_P, &D_Q, &Q_INV).unwrap(); + let bytes = sk.encode(); + assert!(matches!( + RsaPrivateKey::<32, 16>::from_bytes(&bytes[..639]), + Err(SignatureError::DecodingError(_)) + )); + let mut too_long = bytes.to_vec(); + too_long.push(0); + assert!(matches!( + RsaPrivateKey::<32, 16>::from_bytes(&too_long), + Err(SignatureError::DecodingError(_)) + )); +} diff --git a/crypto/rsa/tests/modexp_tests.rs b/crypto/rsa/tests/modexp_tests.rs new file mode 100644 index 000000000..ec6591706 --- /dev/null +++ b/crypto/rsa/tests/modexp_tests.rs @@ -0,0 +1,210 @@ +//! Known-answer and round-trip tests for [`bouncycastle_rsa::modexp`]. +//! +//! Every KAT value below was computed with Python's arbitrary-precision `pow(base, exp, mod)` +//! (not from recall), so this file is the record of that cross-check rather than a duplicate of +//! it: see each case's comment for how it was produced. + +use bouncycastle_rsa::modexp::{ + MontgomeryContext, mod_pow, mont_n_prime, mul_mod, reduce_wide, sub_mod, +}; + +/// `n' = -n0^-1 mod 2^64` must satisfy `n0 * n' ≡ -1 (mod 2^64)` -- the exact identity +/// [`bouncycastle_ec::montgomery::redc`] relies on. Checked directly by wrapping multiplication, +/// across small, large, and adversarial (all-ones) odd values. +#[test] +fn mont_n_prime_satisfies_redc_identity() { + let candidates: [u64; 6] = [1, 3, 0xFFFFFFFFFFFFFFFF, 0x0CA1, 0xDEADBEEFDEADBEEF, u64::MAX - 4]; + for &n0 in &candidates { + let n_prime = mont_n_prime(n0); + assert_eq!( + n0.wrapping_mul(n_prime), + 0u64.wrapping_sub(1), + "n0 * n' must be -1 mod 2^64 for n0 = {n0:#x}" + ); + } +} + +/// Classic textbook RSA (Wikipedia's worked example): p=61, q=53, n=3233, e=17, d=2753, m=65. +/// `c = pow(65, 17, 3233) = 2790` and `pow(2790, 2753, 3233) = 65`, both via Python's `pow`. +#[test] +fn textbook_rsa_l1() { + let n: [u64; 1] = [0x0000000000000ca1]; + let e: [u64; 1] = [0x0000000000000011]; + let d: [u64; 1] = [0x0000000000000ac1]; + let m: [u64; 1] = [0x0000000000000041]; + let expected_c: [u64; 1] = [0x0000000000000ae6]; + + let ctx = MontgomeryContext::<1>::new(&n).expect("3233 is odd"); + let c = mod_pow::<1, 2, 3>(&m, &e, &ctx); + assert_eq!(c, expected_c); + + let decrypted = mod_pow::<1, 2, 3>(&c, &d, &ctx); + assert_eq!(decrypted, m); +} + +/// `L = 1`: `n = 2^64 - 59`, `pow(123456789, 987654321, n)`, via Python. +#[test] +fn kat_l1_large_modulus() { + let n: [u64; 1] = [0xffffffffffffffc5]; + let base: [u64; 1] = [0x00000000075bcd15]; + let exp: [u64; 1] = [0x000000003ade68b1]; + let expected: [u64; 1] = [0xb922a86ec0ff6fe7]; + + let ctx = MontgomeryContext::<1>::new(&n).expect("modulus is odd"); + assert_eq!(mod_pow::<1, 2, 3>(&base, &exp, &ctx), expected); +} + +/// `L = 2` (128-bit modulus `2^127 - 1`), via Python. +#[test] +fn kat_l2_128_bit() { + let n: [u64; 2] = [0xffffffffffffffff, 0x7fffffffffffffff]; + let base: [u64; 2] = [0xc373e0ee4e3f0ad2, 0x000000018ee90ff6]; + let exp: [u64; 2] = [0x0000000000000003, 0x0000000000000000]; + let expected: [u64; 2] = [0xc327da61b1a46a17, 0x02d0b2240ebf02da]; + + let ctx = MontgomeryContext::<2>::new(&n).expect("modulus is odd"); + assert_eq!(mod_pow::<2, 4, 5>(&base, &exp, &ctx), expected); +} + +/// `L = 4` (256-bit random odd, non-prime modulus), via Python (`random.seed(42)`). +#[test] +fn kat_l4_256_bit() { + let n: [u64; 4] = + [0x1c80317fa3b1799d, 0xbdd640fb06671ad1, 0x3eb13b9046685257, 0x23b8c1e9392456de]; + let base: [u64; 4] = + [0xa83c59a92dc37a35, 0xc64362c7939fc228, 0x90d835f3cac497f1, 0x08477cc431b04409]; + let exp: [u64; 4] = + [0x0822e8f36c031199, 0x17fc695a07a0ca6e, 0x3b8faa1837f8a88b, 0x9a1de644815ef6d1]; + let expected: [u64; 4] = + [0x9c1037426d1f8616, 0x7a21504a60dc2678, 0x8f54fb0513201d76, 0x1d2469f43bb3a703]; + + let ctx = MontgomeryContext::<4>::new(&n).expect("modulus is odd"); + assert_eq!(mod_pow::<4, 8, 9>(&base, &exp, &ctx), expected); +} + +/// `L = 2`, a genuine 128-bit RSA keypair (two random 64-bit primes, `e = 65537`), sign/verify +/// round trip. Generated with Python (`random.seed(99)`), including `d = pow(e, -1, phi)`. +#[test] +fn genuine_toy_rsa_keypair_round_trip_l2() { + let n: [u64; 2] = [0x5cdd02d9b55c299d, 0x75acc2c4cc38005e]; + let e: [u64; 1] = [0x0000000000010001]; + let d: [u64; 2] = [0x9254c980da0d8d91, 0x27de4c6ed8b1f08d]; + let m: [u64; 2] = [0xa040024187f1a6ad, 0x624948139eab75cd]; + let expected_c: [u64; 2] = [0x0827db8b026ab9ca, 0x756a056f15b29f68]; + + let ctx = MontgomeryContext::<2>::new(&n).expect("modulus is odd"); + + // Public exponent (17 bits) padded into 2 limbs -- mod_pow processes the full 64*L width + // regardless, so a short public exponent works the same way as a full-width one. + let e_padded: [u64; 2] = [e[0], 0]; + let c = mod_pow::<2, 4, 5>(&m, &e_padded, &ctx); + assert_eq!(c, expected_c); + + let decrypted = mod_pow::<2, 4, 5>(&c, &d, &ctx); + assert_eq!(decrypted, m); +} + +/// `L = 32` (2048-bit modulus, RSA-shaped), via Python (`random.seed(7)`). Exercises the +/// production limb count end to end, not just small hand-checkable cases. +#[test] +fn kat_l32_2048_bit() { + let n: [u64; 32] = [ + 0xf2a74de452e6b439, 0x6513270e269e0d37, 0x0c5c7fd0a6a3a450, 0xd23f0824128b2f33, + 0x1818e811892f902b, 0x9531985d5d9dc9f8, 0xe8e25d940ed90475, 0x36f675cc81e74ef5, + 0x1600a35a099950d8, 0x6b0d549b6f03675a, 0x3d9c172411e20b8f, 0x8d116ece1738f7d9, + 0x0f21ddb66cad4a26, 0x90c192cfd3ac94af, 0xf28c105d1fb17c23, 0xa170b33839263059, + 0x953f48f1a09f76b5, 0x0fd630f1f29d0da9, 0x95e60af593bd04cf, 0x0cb1e29c658cda14, + 0x3898d190f9ebdacc, 0x8e81973e0becd7b0, 0x2217beaddbc496cb, 0x6b4cb2424a23d596, + 0x8a6a63ec24ede6a4, 0x922766581e27a1c0, 0x8f6d05584ef8aa38, 0xae97ba94d0eda82f, + 0x1a61dbe22e44158b, 0x923a736994e3bf91, 0x301850c5a38fd547, 0x98f135d25f557203, + ]; + let base: [u64; 32] = [ + 0xb64ce4228c38fb29, 0x907a70c31012f037, 0x9e7769b10f4205b4, 0x7f15052434b9b5df, + 0x881ed162ae2eb154, 0xc6f877186d76b07e, 0x7731af10506bf2ef, 0xec66a78795e761d1, + 0x5c90a9587403e430, 0x3f98e2774cbd87ad, 0x2e05319acb5c7427, 0xc7a2ea20b2f14c94, + 0x14f4733f3e7d1bfb, 0x4cdd2055930d6eaf, 0x7ebff20686734721, 0x57ee05cde00902c7, + 0x72e6cc3ababced20, 0x9be4bcfc49b64a08, 0x12bd4acefaecbd38, 0x830e07bc1e398f10, + 0x2a3af4d46b0a18e8, 0x5790f82ec1d3fcff, 0xeeeacbe226e87555, 0x6bf46c697d2caf82, + 0xf646e1f40a097c97, 0x13deef86ab1031d0, 0x8ede0d7ac3baea9e, 0xca02135e92b1d3f2, + 0xd17f9acae01f5057, 0x571242425051c1cc, 0x59a54a7bb1fee08f, 0x7f26144b98289fcd, + ]; + let exp: [u64; 32] = [ + 0xcc011cdd9474031b, 0x119a72d174c9df6a, 0x17f5e837d70820fe, 0x451abd81f1d69ed6, + 0xb2715945795e8229, 0x10a3d6b2aa05e11a, 0xbb2d420f0f88080b, 0x4f426dcbb394fb36, + 0x93f448b3a5aa3c81, 0xae658f33fe3b890b, 0x72158370d269a9a5, 0xb774eb5248db40af, + 0xe315128862c33a4f, 0x58d5563dab2cd31e, 0xf0ce583505c6af07, 0x5affb2297631a992, + 0x9c6539382b0537e6, 0x7e62aa0a1df9fd78, 0x37dc76fb0f17a300, 0x49952399c4aaeac1, + 0xbd0561e6211c70cf, 0x65dc9f503f63af83, 0xeab477d26415479c, 0x7f1b103cdf1582b0, + 0x2a96fb1a14a0f9e7, 0x66d2287672fdf202, 0x4720771f8ca81811, 0x230d977ee2257159, + 0x6e36aab0d1bc52d9, 0x8cdb305fdd2e1609, 0xb4d66a3a47469a4d, 0xfc891b4a6a50df4d, + ]; + let expected: [u64; 32] = [ + 0x6d8f5211406eb32d, 0xe8816c035e8d63d0, 0xed5ae634462e4c34, 0xf4f19888a1da251d, + 0xba38c3ca7b518263, 0x7239b204d27d070b, 0x1b1973c8ee44aa69, 0x1ec760c7d18d3043, + 0x8c8261b66b4174f1, 0xc010d589c1eff8fd, 0xdb08314101c4e5d4, 0x626777ea0d794791, + 0x2040c20118bd8914, 0x984acd5d9f9d0eea, 0x6defd175fe17fc42, 0x77ced1e2bdb17dcd, + 0xd6684e6654758d1e, 0xff9e73db6471b299, 0x7429feb8b60a1cd7, 0x9b9740637d41d13d, + 0x008061aa7796f060, 0x1ce3f7b5954e82f4, 0x1d4298d60f58dffd, 0xbcc62dca692679ec, + 0x185d509c9c6c3698, 0x423a2e0c55dec062, 0x0d1b157097b65031, 0x5a6ba1b66d5ca4d8, + 0x0f3cba5f0d7fa3a6, 0x8e6e44eaac15c7e5, 0x62bd4e9b9ccd49c3, 0x915f6c85d08df86a, + ]; + + let ctx = MontgomeryContext::<32>::new(&n).expect("modulus is odd"); + assert_eq!(mod_pow::<32, 64, 65>(&base, &exp, &ctx), expected); +} + +/// An even modulus is never valid for Montgomery reduction; RSA moduli are always odd, so this +/// is purely a defensive boundary check. +#[test] +fn even_modulus_is_rejected() { + let n: [u64; 1] = [4]; + assert!(MontgomeryContext::<1>::new(&n).is_none()); +} + +/// `reduce_wide` with `WIDE = 4 > NARROW = 2`, RSA's own use for reducing an `n`-width message +/// down to a CRT prime's width (RFC 8017 §5.2.1 step 2.b.1). Via Python's `%`. +#[test] +fn reduce_wide_wider_value() { + let value: [u64; 4] = + [0x8899aabbccddeeff, 0x0011223344556677, 0xfedcba9876543210, 0x123456789abcdef0]; + let modulus: [u64; 2] = [0xffffffffffffff61, 0xffffffffffffffff]; + let expected: [u64; 2] = [0xd3b18f6d4b290dc4, 0x4e92d71b5fa3de25]; + assert_eq!(reduce_wide::<4, 2>(&value, &modulus), expected); +} + +/// `reduce_wide` with `WIDE == NARROW`, deliberately far outside the `< 2 * modulus` bound a +/// single conditional subtraction would need: `value` is about 4.9 billion times `modulus`. RSA's +/// CRT recombination (step 2.b.3, `s2 mod p`) uses exactly this shape, and RFC 8017 does not +/// bound how large `q`'s residues can be relative to `p` (see `RsaPrivateKey`'s docs) -- this is +/// the case that matters, not the same-limb-count part of the signature. Via Python's `%`. +#[test] +fn reduce_wide_same_width_but_far_outside_double_the_modulus() { + let value: [u64; 2] = [0xfffffffffffffffd, 0xfffffffffffffffe]; + let modulus: [u64; 2] = [0x0000000000000101, 0x00000000deadbeef]; + let expected: [u64; 2] = [0xfffffed88afbe598, 0x00000000a6a2b0b3]; + assert_eq!(reduce_wide::<2, 2>(&value, &modulus), expected); +} + +/// `sub_mod` when `a < b` (the difference must wrap around by adding the modulus once). Via +/// Python's `%`. +#[test] +fn sub_mod_wraps_when_a_less_than_b() { + let a: [u64; 2] = [0x0000000000000005, 0x0000000000000000]; + let b: [u64; 2] = [0x0000000000003039, 0x0000000000000000]; + let n: [u64; 2] = [0xffffffffffffff61, 0xffffffffffffffff]; + let expected: [u64; 2] = [0xffffffffffffcf2d, 0xffffffffffffffff]; + assert_eq!(sub_mod::<2>(&a, &b, &n), expected); +} + +/// `mul_mod` with `a >= modulus` -- only `b` is required to be pre-reduced (see the function's +/// docs on why `a` need not be). Via Python's `%`. +#[test] +fn mul_mod_with_unreduced_first_operand() { + let a: [u64; 2] = [0xfffffffffffffffd, 0xfffffffffffffffe]; + let b: [u64; 2] = [0x23456789abcdef01, 0x0000000000000001]; + let n: [u64; 2] = [0xffffffffffffff61, 0xffffffffffffffff]; + let expected: [u64; 2] = [0x7e4b17e4b17da35e, 0xdcba9876543211b0]; + + let ctx = MontgomeryContext::<2>::new(&n).expect("modulus is odd"); + assert_eq!(mul_mod::<2, 4, 5>(&a, &b, &ctx), expected); +} diff --git a/crypto/rsa/tests/rsa_1024_tests.rs b/crypto/rsa/tests/rsa_1024_tests.rs new file mode 100644 index 000000000..e6737d2eb --- /dev/null +++ b/crypto/rsa/tests/rsa_1024_tests.rs @@ -0,0 +1,176 @@ +//! RSA-1024 (verification only, see `rsa_1024.rs`'s docs) against Wycheproof's +//! `rsa_pkcs1_1024_sig_gen_test.json`. Since this crate never constructs an RSA-1024 private key, +//! there is nothing to factor here: a sig-gen file's own `privateKey.modulus`/`publicExponent` +//! fields are simultaneously valid *public* key material, so each group's `(n, e)` plus its own +//! `(msg, sig)` pairs are used directly as real, independent "valid" verify vectors -- exactly +//! what `rsa_signature_*_test.json` files provide at every other size, just recovered from a +//! differently-shaped file because Wycheproof has no dedicated `rsa_signature_1024_*.json`. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::VerifyingKey; +use bouncycastle_core::traits::SignaturePublicKey; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rsa::rsa_1024::{ + PK_LEN, RSA1024PublicKey, RSASSA_PKCS1_v1_5_SHA256, RSASSA_PKCS1_v1_5_SHA384, +}; +use serde_json::Value; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +fn limbs_from_hex(hex: &str) -> [u64; L] { + let bytes_len = 8 * L; + let mut bytes = hex_decode(hex).expect("valid hex"); + if bytes.len() == bytes_len + 1 && bytes[0] == 0 { + bytes.remove(0); + } + assert_eq!(bytes.len(), bytes_len, "expected a {}-bit value", bytes_len * 8); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = bytes_len - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + limbs +} + +fn run_sig_gen_group_as_verify_vectors( + sha: &str, + verify: impl Fn(&RSA1024PublicKey, &[u8], &[u8; 128]) -> bool, +) { + let doc: Value = + serde_json::from_str(&get_test_data("rsa_pkcs1_1024_sig_gen_test.json")).unwrap(); + let group = doc["testGroups"] + .as_array() + .unwrap() + .iter() + .find(|g| g["sha"] == sha && g["tests"].as_array().unwrap().len() == 8) + .unwrap_or_else(|| panic!("the main {sha}/1024 group")); + + let n: [u64; 16] = limbs_from_hex(group["privateKey"]["modulus"].as_str().unwrap()); + let e = + u32::from_str_radix(group["privateKey"]["publicExponent"].as_str().unwrap(), 16).unwrap(); + let pk = RSA1024PublicKey::new(&n, e).expect("group public key must be valid"); + + let mut num_tests = 0usize; + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let sig: [u8; 128] = sig_bytes.try_into().expect("sig-gen signatures are always k octets"); + assert!(verify(&pk, &msg, &sig), "tcId {tc_id}: a genuine signature must verify"); + } + assert_eq!(num_tests, 8); +} + +#[test] +fn pkcs1_v1_5_sha256_accepts_genuine_sig_gen_signatures() { + run_sig_gen_group_as_verify_vectors("SHA-256", |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }); +} + +#[test] +fn pkcs1_v1_5_sha384_accepts_genuine_sig_gen_signatures() { + run_sig_gen_group_as_verify_vectors("SHA-384", |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }); +} + +fn run_sig_gen_group_rejects_wrong_message( + sha: &str, + verify: impl Fn(&RSA1024PublicKey, &[u8], &[u8; 128]) -> bool, +) { + let doc: Value = + serde_json::from_str(&get_test_data("rsa_pkcs1_1024_sig_gen_test.json")).unwrap(); + let group = doc["testGroups"] + .as_array() + .unwrap() + .iter() + .find(|g| g["sha"] == sha && g["tests"].as_array().unwrap().len() == 8) + .unwrap(); + let n: [u64; 16] = limbs_from_hex(group["privateKey"]["modulus"].as_str().unwrap()); + let e = + u32::from_str_radix(group["privateKey"]["publicExponent"].as_str().unwrap(), 16).unwrap(); + let pk = RSA1024PublicKey::new(&n, e).unwrap(); + let test = &group["tests"][0]; + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let sig: [u8; 128] = sig_bytes.try_into().unwrap(); + assert!(!verify(&pk, b"a message this signature was not made for", &sig)); +} + +#[test] +fn pkcs1_v1_5_sha256_rejects_wrong_message() { + run_sig_gen_group_rejects_wrong_message("SHA-256", |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }); +} + +/// Mutation testing found that PKCS#1 v1.5/SHA-384 verification had no rejection-path test of its own +/// (only `verify_sha256`'s did) -- a whole-function-body mutant that always returned `Ok(())` +/// still passed the whole suite. +#[test] +fn pkcs1_v1_5_sha384_rejects_wrong_message() { + run_sig_gen_group_rejects_wrong_message("SHA-384", |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }); +} + +// ---- bouncycastle_core trait conformance ------------------------------------------------------ +// +// No signing role exists at this size (see `rsa_1024.rs`'s docs and its `compile_fail` doctest), so +// `core-test-framework`'s signature suite -- which needs one -- cannot run here; the +// `VerifyingKey` and `SignaturePublicKey` impls are checked directly against the same +// genuine Wycheproof material as the tests above. + +/// RFC 8017 §8.2.2 step 1 through the trait's `&[u8]` signature: a truncated genuine signature +/// is rejected for its length, never accepted. +#[test] +fn trait_verify_rejects_truncated_signature() { + run_sig_gen_group_as_verify_vectors("SHA-256", |pk, msg, sig| { + matches!( + VerifyingKey::::verify(pk, msg, None, &sig[..127]), + Err(SignatureError::LengthError(_)) + ) + }); +} + +#[test] +fn public_key_trait_encoding_round_trips_and_rejects_wrong_lengths() { + let doc: Value = + serde_json::from_str(&get_test_data("rsa_pkcs1_1024_sig_gen_test.json")).unwrap(); + let group = &doc["testGroups"][0]; + let n: [u64; 16] = limbs_from_hex(group["privateKey"]["modulus"].as_str().unwrap()); + let e = + u32::from_str_radix(group["privateKey"]["publicExponent"].as_str().unwrap(), 16).unwrap(); + let pk = RSA1024PublicKey::new(&n, e).unwrap(); + + let bytes = pk.encode(); + assert_eq!(bytes.len(), PK_LEN); + assert_eq!(RSA1024PublicKey::from_bytes(&bytes).unwrap(), pk); + assert!(matches!( + RSA1024PublicKey::from_bytes(&bytes[..PK_LEN - 1]), + Err(SignatureError::DecodingError(_)) + )); + let mut too_long = bytes.to_vec(); + too_long.push(0); + assert!(matches!( + RSA1024PublicKey::from_bytes(&too_long), + Err(SignatureError::DecodingError(_)) + )); +} diff --git a/crypto/rsa/tests/rsa_1536_tests.rs b/crypto/rsa/tests/rsa_1536_tests.rs new file mode 100644 index 000000000..d17c08a3e --- /dev/null +++ b/crypto/rsa/tests/rsa_1536_tests.rs @@ -0,0 +1,180 @@ +//! RSA-1536 (verification only, see `rsa_1536.rs`'s docs) against Wycheproof's +//! `rsa_pkcs1_1536_sig_gen_test.json`, the same way `rsa_1024_tests.rs` uses its own sig-gen file: +//! each group's `(n, e)` plus its own `(msg, sig)` pairs, used directly as real "valid" verify +//! vectors, since a sig-gen file's private key fields are simultaneously valid public key +//! material and this crate never constructs an RSA-1536 private key at all. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::VerifyingKey; +use bouncycastle_core::traits::SignaturePublicKey; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rsa::rsa_1536::{ + PK_LEN, RSA1536PublicKey, RSASSA_PKCS1_v1_5_SHA256, RSASSA_PKCS1_v1_5_SHA384, + RSASSA_PKCS1_v1_5_SHA512, +}; +use serde_json::Value; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +fn limbs_from_hex(hex: &str) -> [u64; L] { + let bytes_len = 8 * L; + let mut bytes = hex_decode(hex).expect("valid hex"); + if bytes.len() == bytes_len + 1 && bytes[0] == 0 { + bytes.remove(0); + } + assert_eq!(bytes.len(), bytes_len, "expected a {}-bit value", bytes_len * 8); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = bytes_len - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + limbs +} + +fn run_sig_gen_group_as_verify_vectors( + sha: &str, + verify: impl Fn(&RSA1536PublicKey, &[u8], &[u8; 192]) -> bool, +) { + let doc: Value = + serde_json::from_str(&get_test_data("rsa_pkcs1_1536_sig_gen_test.json")).unwrap(); + let group = doc["testGroups"] + .as_array() + .unwrap() + .iter() + .find(|g| g["sha"] == sha) + .unwrap_or_else(|| panic!("the {sha}/1536 group")); + + let n: [u64; 24] = limbs_from_hex(group["privateKey"]["modulus"].as_str().unwrap()); + let e = + u32::from_str_radix(group["privateKey"]["publicExponent"].as_str().unwrap(), 16).unwrap(); + let pk = RSA1536PublicKey::new(&n, e).expect("group public key must be valid"); + + let mut num_tests = 0usize; + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let sig: [u8; 192] = sig_bytes.try_into().expect("sig-gen signatures are always k octets"); + assert!(verify(&pk, &msg, &sig), "tcId {tc_id}: a genuine signature must verify"); + } + assert_eq!(num_tests, 8); +} + +#[test] +fn pkcs1_v1_5_sha256_accepts_genuine_sig_gen_signatures() { + run_sig_gen_group_as_verify_vectors("SHA-256", |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }); +} + +#[test] +fn pkcs1_v1_5_sha384_accepts_genuine_sig_gen_signatures() { + run_sig_gen_group_as_verify_vectors("SHA-384", |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }); +} + +#[test] +fn pkcs1_v1_5_sha512_accepts_genuine_sig_gen_signatures() { + run_sig_gen_group_as_verify_vectors("SHA-512", |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }); +} + +fn run_sig_gen_group_rejects_wrong_message( + sha: &str, + verify: impl Fn(&RSA1536PublicKey, &[u8], &[u8; 192]) -> bool, +) { + let doc: Value = + serde_json::from_str(&get_test_data("rsa_pkcs1_1536_sig_gen_test.json")).unwrap(); + let group = doc["testGroups"].as_array().unwrap().iter().find(|g| g["sha"] == sha).unwrap(); + let n: [u64; 24] = limbs_from_hex(group["privateKey"]["modulus"].as_str().unwrap()); + let e = + u32::from_str_radix(group["privateKey"]["publicExponent"].as_str().unwrap(), 16).unwrap(); + let pk = RSA1536PublicKey::new(&n, e).unwrap(); + let test = &group["tests"][0]; + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let sig: [u8; 192] = sig_bytes.try_into().unwrap(); + assert!(!verify(&pk, b"a message this signature was not made for", &sig)); +} + +#[test] +fn pkcs1_v1_5_sha256_rejects_wrong_message() { + run_sig_gen_group_rejects_wrong_message("SHA-256", |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }); +} + +/// Mutation testing found that PKCS#1 v1.5/SHA-384 and /SHA-512 verification had no rejection-path test of +/// their own (only `verify_sha256`'s did) -- a whole-function-body mutant that always returned +/// `Ok(())` still passed the whole suite for either. +#[test] +fn pkcs1_v1_5_sha384_rejects_wrong_message() { + run_sig_gen_group_rejects_wrong_message("SHA-384", |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }); +} + +#[test] +fn pkcs1_v1_5_sha512_rejects_wrong_message() { + run_sig_gen_group_rejects_wrong_message("SHA-512", |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }); +} + +// ---- bouncycastle_core trait conformance ------------------------------------------------------ +// +// Verify-only at this size, as at RSA-1024 -- see `rsa_1024_tests.rs` for why the +// `core-test-framework` signature suite cannot run here. + +#[test] +fn trait_verify_rejects_truncated_signature() { + run_sig_gen_group_as_verify_vectors("SHA-256", |pk, msg, sig| { + matches!( + VerifyingKey::::verify(pk, msg, None, &sig[..191]), + Err(SignatureError::LengthError(_)) + ) + }); +} + +#[test] +fn public_key_trait_encoding_round_trips_and_rejects_wrong_lengths() { + let doc: Value = + serde_json::from_str(&get_test_data("rsa_pkcs1_1536_sig_gen_test.json")).unwrap(); + let group = &doc["testGroups"][0]; + let n: [u64; 24] = limbs_from_hex(group["privateKey"]["modulus"].as_str().unwrap()); + let e = + u32::from_str_radix(group["privateKey"]["publicExponent"].as_str().unwrap(), 16).unwrap(); + let pk = RSA1536PublicKey::new(&n, e).unwrap(); + + let bytes = pk.encode(); + assert_eq!(bytes.len(), PK_LEN); + assert_eq!(RSA1536PublicKey::from_bytes(&bytes).unwrap(), pk); + assert!(matches!( + RSA1536PublicKey::from_bytes(&bytes[..PK_LEN - 1]), + Err(SignatureError::DecodingError(_)) + )); + let mut too_long = bytes.to_vec(); + too_long.push(0); + assert!(matches!( + RSA1536PublicKey::from_bytes(&too_long), + Err(SignatureError::DecodingError(_)) + )); +} diff --git a/crypto/rsa/tests/rsa_2048_pkcs1_v1_5_sha384_sha512_tests.rs b/crypto/rsa/tests/rsa_2048_pkcs1_v1_5_sha384_sha512_tests.rs new file mode 100644 index 000000000..879e0f4ce --- /dev/null +++ b/crypto/rsa/tests/rsa_2048_pkcs1_v1_5_sha384_sha512_tests.rs @@ -0,0 +1,237 @@ +//! RSA-2048 RSASSA-PKCS1-v1_5 with SHA-384 and SHA-512, against Wycheproof's +//! `rsa_signature_2048_sha384_test.json`/`rsa_signature_2048_sha512_test.json`. +//! +//! Each of Wycheproof's `rsa_pkcs1_2048_sig_gen_test.json` groups (including its SHA-384 and +//! SHA-512 ones) uses a *distinct* key -- unlike the SHA-256 case +//! (`rsa_2048_pkcs1_v1_5_tests.rs`), there is no shared key to recover once and reuse across +//! hashes here, and factoring another key per hash is disproportionate to what this step needs: +//! `EMSA-PKCS1-v1_5`'s DigestInfo construction is generic over the hash (see `digest_info`'s own +//! docs) and already exercised end to end for SHA-256, and RSASP1/RSAVP1 do not depend on which +//! hash produced the message representative they are given. So the sign side here is a +//! self-consistency round trip against the SHA-256 key `rsa_2048_pkcs1_v1_5_tests.rs` already +//! recovered (a valid RSA private key signs under any hash choice; nothing about the key ties it +//! to SHA-256), while the verify side -- where the real risk is a wrong OID or digest length for +//! the hash in question -- is checked against all of Wycheproof's real vectors for both hashes. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{DeterministicSigningKey, VerifyingKey}; +use bouncycastle_core_test_framework::signature::TestFrameworkSignature; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rsa::rsa_2048::{ + RSA2048PrivateKey, RSA2048PublicKey, RSASSA_PKCS1_v1_5_SHA384, RSASSA_PKCS1_v1_5_SHA512, + SIG_LEN, +}; +use serde_json::Value; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +fn limbs_from_hex(hex: &str) -> [u64; L] { + let bytes_len = 8 * L; + let mut bytes = hex_decode(hex).expect("valid hex"); + if bytes.len() == bytes_len + 1 && bytes[0] == 0 { + bytes.remove(0); + } + assert_eq!(bytes.len(), bytes_len, "expected a {}-bit value", bytes_len * 8); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = bytes_len - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + limbs +} + +/// The same genuine RSA-2048 keypair `rsa_2048_pkcs1_v1_5_tests.rs` recovers (from +/// `rsa_pkcs1_2048_sig_gen_test.json`'s SHA-256 group) -- reused here as a plain, valid RSA-2048 +/// key, not because it has anything to do with SHA-256 specifically. +fn genuine_key() -> RSA2048PrivateKey { + let p: [u64; 16] = [ + 0x0ea36cfb3a5b18f1, 0x48a6e65332119129, 0x110ad9e7b48a1c93, 0x569156b90113e2e9, + 0xe79813a575cfad9c, 0x69d659d143ec6f17, 0xe81e6bab5ddaa783, 0xbff1c5b80a69f788, + 0x978f6c35814f50ee, 0xe6a289ad4cfbf78f, 0x34d5681e5809d415, 0xbb028bda42eeb5d2, + 0x41c56e4de086b0d5, 0x58b8d1e24f3b55d0, 0xfb5248247d98cb7d, 0xdc431050f782e894, + ]; + let q: [u64; 16] = [ + 0x669f140cfbc20f25, 0xb97bb03677207d95, 0xfd4e06f3ed7299d4, 0x160f90536abc9492, + 0xf5b131f39098f7bc, 0xae8d72c57088d7ab, 0x89b94fbde542aba9, 0x3d3f9880ec47d5e0, + 0x1378a6868af3b7a0, 0x5544070beb057c94, 0x16611debc472fac4, 0xe500ffb79f5b8868, + 0x308a5e32196603b2, 0xea5fb19eb4eabc38, 0x122273ae3222b598, 0xbd1a81e7977f9898, + ]; + let d_p: [u64; 16] = [ + 0x209f33f09515d7c1, 0xb4a9b37656917205, 0x276933bb07e4efb9, 0x8c14019808e00414, + 0x289f96da220711e5, 0xfbbd2923d31532fe, 0xc06b414e61c0e1e7, 0x4c23c4588488961d, + 0x4dc48ae34514759c, 0x9c786961ae3e2c35, 0x497e8d9c650688e0, 0x18bf08472612dbe5, + 0x8885fb161870ee12, 0xf21d7c1479d99d47, 0x9121d91952ffd1c7, 0xa94b528b28f29159, + ]; + let d_q: [u64; 16] = [ + 0xf7597ffb68011d8d, 0x7b3cc538c4bab8c9, 0xa8fa480a81a925af, 0x6d6ede7251a383bf, + 0x8a63f788ce3a0f85, 0x0b920502eb478bc9, 0x7e37e755edfe70d9, 0x9cf9948422a16555, + 0x0d6d9ea1f2ef71fd, 0xf7efa32ea0cb6e00, 0x0629b114ca7f780f, 0xcf51176359654348, + 0x540cdcbd4ad35435, 0x31c02ff1a2bc437c, 0xff2503df78bafed5, 0x3af0e72a933aef09, + ]; + let q_inv: [u64; 16] = [ + 0x552fe4bfce945f7b, 0x67e50c999c67247b, 0xfb54ef17be3b2853, 0x241f5921b5ad3983, + 0x02de5eccd143cf31, 0x74e45f6fcc60f216, 0xafa5428a74f12708, 0x88d42294b6a2759b, + 0xe923e1097c0c562f, 0xc968b48a91c38b5b, 0x933e85179c0320b0, 0x7993d0445f758d51, + 0x9bfc042ee0924b1b, 0x41f956d90fa8a793, 0xee7a87b6483a66ee, 0x2640fbfbcfefb163, + ]; + RSA2048PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("recovered CRT components must be accepted") +} + +#[test] +fn pkcs1_v1_5_sha384_round_trips() { + let sk = genuine_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + let sig = DeterministicSigningKey::::sign_deterministic( + &sk, b"hello", None, + ) + .expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig) + .expect("must verify"); + assert!( + VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err() + ); +} + +#[test] +fn pkcs1_v1_5_sha512_round_trips() { + let sk = genuine_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + let sig = DeterministicSigningKey::::sign_deterministic( + &sk, b"hello", None, + ) + .expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig) + .expect("must verify"); + assert!( + VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err() + ); +} + +fn run_verify_vectors( + filename: &str, + verify: impl Fn(&RSA2048PublicKey, &[u8], &[u8; 256]) -> bool, + expected_sha: &str, + expected_num_tests: usize, + expected_num_valid: usize, + expected_num_invalid: usize, +) { + let doc: Value = serde_json::from_str(&get_test_data(filename)).expect("valid JSON"); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + let mut num_missing_null = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], expected_sha); + let n: [u64; 32] = limbs_from_hex(group["publicKey"]["modulus"].as_str().unwrap()); + let e = u32::from_str_radix(group["publicKey"]["publicExponent"].as_str().unwrap(), 16) + .expect("publicExponent fits in u32 for every group here"); + let pk = RSA2048PublicKey::new(&n, e).expect("group public key must be valid"); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let flags: Vec<&str> = + test["flags"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect(); + + let Ok(sig): Result<[u8; 256], _> = sig_bytes.try_into() else { + assert_ne!(test["result"], "valid", "tcId {tc_id}: wrong-length 'valid' signature"); + num_invalid += 1; + continue; + }; + + let verified = verify(&pk, &msg, &sig); + match test["result"].as_str().unwrap() { + "valid" => { + assert!(verified, "tcId {tc_id}: expected valid, got invalid"); + num_valid += 1; + } + "invalid" => { + assert!(!verified, "tcId {tc_id}: expected invalid, got valid"); + num_invalid += 1; + } + "acceptable" => { + assert_eq!( + flags, + vec!["MissingNull"], + "tcId {tc_id}: unreviewed 'acceptable' flag combination {flags:?}" + ); + assert!(verified, "tcId {tc_id}: MissingNull must be accepted"); + num_missing_null += 1; + } + other => panic!("tcId {tc_id}: unknown result {other:?}"), + } + } + } + + assert_eq!(num_tests, expected_num_tests); + assert_eq!(num_missing_null, 1); + assert_eq!(num_valid, expected_num_valid); + assert_eq!(num_invalid, expected_num_invalid); +} + +#[test] +fn rsa_signature_sha384_wycheproof_vectors() { + run_verify_vectors( + "rsa_signature_2048_sha384_test.json", + |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }, + "SHA-384", + 258, + 7, + 250, + ); +} + +#[test] +fn rsa_signature_sha512_wycheproof_vectors() { + run_verify_vectors( + "rsa_signature_2048_sha512_test.json", + |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }, + "SHA-512", + 259, + 8, + 250, + ); +} + +// ---- bouncycastle_core trait conformance ------------------------------------------------------ + +fn fixed_keypair() -> Result<(RSA2048PublicKey, RSA2048PrivateKey), SignatureError> { + let sk = genuine_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001)?; + Ok((pk, sk)) +} + +/// `core-test-framework`'s conformance suite for the SHA-384 and SHA-512 pairings (deterministic, +/// `ctx` ignored). The exhaustive bit-flip pass runs in `rsa_2048_pkcs1_v1_5_tests.rs`'s SHA-256 +/// suite, over the same generic code. +#[test] +fn pkcs1_v1_5_sha384_sha512_trait_conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework.test_deterministic_signature::(fixed_keypair, false); + framework.test_deterministic_signature::(fixed_keypair, false); +} diff --git a/crypto/rsa/tests/rsa_2048_pkcs1_v1_5_tests.rs b/crypto/rsa/tests/rsa_2048_pkcs1_v1_5_tests.rs new file mode 100644 index 000000000..2f53e653d --- /dev/null +++ b/crypto/rsa/tests/rsa_2048_pkcs1_v1_5_tests.rs @@ -0,0 +1,311 @@ +//! RSA-2048/SHA-256 RSASSA-PKCS1-v1_5 against Wycheproof (`rsa_pkcs1_2048_sig_gen_test.json` and +//! `rsa_signature_2048_sha256_test.json`), following the path/lookup convention +//! `bouncycastle-ecdsa`'s `tests/wycheproof.rs` documents (symlink the clone at +//! `/tmp/wycheproof` for `cargo mutants`). +//! +//! No CAVP SigGen15/SigVer15 vectors exist in this workspace's `bc-test-data` clone at the time +//! of writing (only ECDSA/KDF material does), so Wycheproof alone carries both directions here: +//! `rsa_pkcs1_2048_sig_gen_test.json` for signing (deterministic PKCS#1 v1.5 has exactly one +//! correct signature per message, same as a CAVP SigGen KAT) and `rsa_signature_2048_sha256_test.json` +//! for verification. +//! +//! # Where the private key comes from +//! +//! Wycheproof's sig-gen vectors give `(n, e, d)`, not the CRT quintuple +//! [`RsaPrivateKey::from_crt_components`] needs. `p` and `q` were recovered from `(n, e, d)` in +//! Python via the standard randomized factoring-from-the-private-exponent method (Boneh, *Twenty +//! Years of Attacks on the RSA Cryptosystem*, 1999, §I.B; originally Miller, 1975): `e*d - 1` is a +//! multiple of `λ(n)`, which a few rounds of a Miller-Rabin-shaped search turn into a nontrivial +//! square root of `1 mod n`, and `gcd(that - 1, n)` is a factor with high probability. `dP`, `dQ`, +//! `qInv` were then computed directly from `p`, `q`, `d`, `e`. The recovered CRT key was +//! cross-checked in Python against all eight of this group's vectors (CRT recombination +//! reproducing the exact `sig` bytes) before being pasted here -- this file's own tests are the +//! same check again, in Rust, against RSASSA-PKCS1-v1_5's real API. +//! +//! This factoring step lives only in the throwaway script that produced these constants, never in +//! the crate: `bouncycastle-rsa` has no prime-generation or factoring code (see `src/lib.rs`'s +//! `# Scope`), by design. +//! +//! `p` and `q` here are both genuinely 1024 bits (this key's generator produced balanced primes, +//! as most do) -- [`RsaPrivateKey`] does not require that, see its docs, but this file cannot +//! itself demonstrate the unbalanced case: it works from a real generator's output, not a +//! constructed edge case. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{DeterministicSigningKey, VerifyingKey}; +use bouncycastle_core::traits::{HashOneShot, SignatureSigner}; +use bouncycastle_core_test_framework::signature::{ + TestFrameworkSignature, TestFrameworkSignatureKeys, +}; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rsa::rsa_2048::{ + PK_LEN, RSA2048PrivateKey, RSA2048PublicKey, RSASSA_PKCS1_v1_5_SHA256, SIG_LEN, SK_LEN, +}; +use bouncycastle_rsa::rsassa_pkcs1_v1_5; +use bouncycastle_sha2::SHA256; +use serde_json::Value; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +/// Big-endian hex (as Wycheproof/ASN.1 write it, with an optional leading `00` sign-avoidance +/// byte) into this crate's little-endian `[u64; L]` limb form. Duplicated in miniature from +/// `bouncycastle_rsa::codec` (crate-private) rather than exposed from it: this conversion belongs +/// to the test's own data plumbing, not to the crate's public API surface. +fn limbs_from_hex(hex: &str) -> [u64; L] { + let bytes_len = 8 * L; + let mut bytes = hex_decode(hex).expect("valid hex"); + if bytes.len() == bytes_len + 1 && bytes[0] == 0 { + bytes.remove(0); + } + assert_eq!(bytes.len(), bytes_len, "expected a {}-bit value", bytes_len * 8); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = bytes_len - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + limbs +} + +fn wycheproof_key() -> RSA2048PrivateKey { + // Little-endian [u64; 16] limbs -- P[0] is p's *least* significant 64 bits. (An earlier + // version of this file tried to write these as a single big-endian hex string by + // concatenating the limbs in array order, which silently reverses significance at each 8-byte + // boundary and produces a different, even number; `limbs_from_hex` is for genuinely + // big-endian input, such as the JSON's own hex fields, not for re-deriving these.) + let p: [u64; 16] = [ + 0x0ea36cfb3a5b18f1, 0x48a6e65332119129, 0x110ad9e7b48a1c93, 0x569156b90113e2e9, + 0xe79813a575cfad9c, 0x69d659d143ec6f17, 0xe81e6bab5ddaa783, 0xbff1c5b80a69f788, + 0x978f6c35814f50ee, 0xe6a289ad4cfbf78f, 0x34d5681e5809d415, 0xbb028bda42eeb5d2, + 0x41c56e4de086b0d5, 0x58b8d1e24f3b55d0, 0xfb5248247d98cb7d, 0xdc431050f782e894, + ]; + let q: [u64; 16] = [ + 0x669f140cfbc20f25, 0xb97bb03677207d95, 0xfd4e06f3ed7299d4, 0x160f90536abc9492, + 0xf5b131f39098f7bc, 0xae8d72c57088d7ab, 0x89b94fbde542aba9, 0x3d3f9880ec47d5e0, + 0x1378a6868af3b7a0, 0x5544070beb057c94, 0x16611debc472fac4, 0xe500ffb79f5b8868, + 0x308a5e32196603b2, 0xea5fb19eb4eabc38, 0x122273ae3222b598, 0xbd1a81e7977f9898, + ]; + let d_p: [u64; 16] = [ + 0x209f33f09515d7c1, 0xb4a9b37656917205, 0x276933bb07e4efb9, 0x8c14019808e00414, + 0x289f96da220711e5, 0xfbbd2923d31532fe, 0xc06b414e61c0e1e7, 0x4c23c4588488961d, + 0x4dc48ae34514759c, 0x9c786961ae3e2c35, 0x497e8d9c650688e0, 0x18bf08472612dbe5, + 0x8885fb161870ee12, 0xf21d7c1479d99d47, 0x9121d91952ffd1c7, 0xa94b528b28f29159, + ]; + let d_q: [u64; 16] = [ + 0xf7597ffb68011d8d, 0x7b3cc538c4bab8c9, 0xa8fa480a81a925af, 0x6d6ede7251a383bf, + 0x8a63f788ce3a0f85, 0x0b920502eb478bc9, 0x7e37e755edfe70d9, 0x9cf9948422a16555, + 0x0d6d9ea1f2ef71fd, 0xf7efa32ea0cb6e00, 0x0629b114ca7f780f, 0xcf51176359654348, + 0x540cdcbd4ad35435, 0x31c02ff1a2bc437c, 0xff2503df78bafed5, 0x3af0e72a933aef09, + ]; + let q_inv: [u64; 16] = [ + 0x552fe4bfce945f7b, 0x67e50c999c67247b, 0xfb54ef17be3b2853, 0x241f5921b5ad3983, + 0x02de5eccd143cf31, 0x74e45f6fcc60f216, 0xafa5428a74f12708, 0x88d42294b6a2759b, + 0xe923e1097c0c562f, 0xc968b48a91c38b5b, 0x933e85179c0320b0, 0x7993d0445f758d51, + 0x9bfc042ee0924b1b, 0x41f956d90fa8a793, 0xee7a87b6483a66ee, 0x2640fbfbcfefb163, + ]; + RSA2048PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("recovered CRT components must be accepted") +} + +/// The first (257-test) group of `rsa_signature_2048_sha256_test.json` uses this exact key +/// (confirmed by comparing moduli in Python before writing this file); the other two groups use +/// unrelated keys and are covered separately by [`rsa_signature_sha256_all_groups`], which parses +/// the public key straight out of each group's JSON instead. +#[test] +fn pkcs1_v1_5_sig_gen_2048_sha256() { + let sk = wycheproof_key(); + + let doc: Value = serde_json::from_str(&get_test_data("rsa_pkcs1_2048_sig_gen_test.json")) + .expect("valid JSON"); + let group = doc["testGroups"] + .as_array() + .unwrap() + .iter() + .find(|g| g["sha"] == "SHA-256" && g["tests"].as_array().unwrap().len() == 8) + .expect("the main SHA-256/2048 group"); + + let mut num_tests = 0usize; + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let expected_sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + assert_eq!(test["result"], "valid", "tcId {tc_id}: sig-gen vectors are all 'valid'"); + + let sig = DeterministicSigningKey::::sign_deterministic( + &sk, &msg, None, + ) + .unwrap_or_else(|e| { + panic!("tcId {tc_id}: signing failed: {e:?}"); + }); + assert_eq!(sig.to_vec(), expected_sig, "tcId {tc_id}: signature mismatch"); + } + assert_eq!(num_tests, 8); +} + +// ---- bouncycastle_core trait conformance ------------------------------------------------------ + +/// The same genuine key pair as the sig-gen test above, in the `fn() -> Result<(PK, SK)>` +/// shape `core-test-framework` takes in place of a key generator (this crate has none). +fn fixed_keypair() -> Result<(RSA2048PublicKey, RSA2048PrivateKey), SignatureError> { + let sk = wycheproof_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001)?; + Ok((pk, sk)) +} + +/// The shared signature-key conformance suite (per QUALITY_AND_STYLE's rule that +/// trait conformance lives in `core-test-framework`, not per implementation): deterministic +/// (PKCS#1 v1.5 has no randomness), `ctx` ignored (RSA has no context input), and with every bit +/// of a signature flipped in turn -- 2048 verifications, run at this size and scheme since the +/// generic `RSASSA_PKCS1_v1_5` code under test is the same at every width. +#[test] +fn pkcs1_v1_5_sha256_trait_conformance_suite() { + TestFrameworkSignature::new(false).test_deterministic_signature::(fixed_keypair, true); +} + +#[test] +fn key_trait_boundary_conditions() { + TestFrameworkSignatureKeys::new() + .test_keys::(fixed_keypair); +} + +/// Streamed and one-shot signing are one computation: PKCS#1 v1.5 is deterministic, so the +/// chunked `create_signer_deterministic`/`do_update`/`do_final` path reproduces `sign_deterministic` byte for byte. +#[test] +fn streamed_signature_equals_one_shot() { + let (pk, sk) = fixed_keypair().unwrap(); + let msg = b"the same message, two ways"; + let one_shot = DeterministicSigningKey::::sign_deterministic( + &sk, msg, None, + ) + .unwrap(); + let mut signer = + DeterministicSigningKey::::create_signer_deterministic( + &sk, None, + ) + .unwrap(); + signer.do_update(&msg[..10]); + signer.do_update(&msg[10..]); + let streamed = signer.do_final().unwrap(); + assert_eq!(streamed, one_shot); + VerifyingKey::::verify(&pk, msg, None, &streamed).unwrap(); +} + +/// RFC 8017 §8.2.2 step 1 ("If the length of the signature S is not k octets, output 'invalid +/// signature'"): the trait's `&[u8]` signature makes this a runtime check, reported as a +/// `LengthError` as the other signature crates do. `core-test-framework` covers the too-long +/// case; this is the too-short one. +#[test] +fn trait_verify_rejects_short_signature() { + let (pk, sk) = fixed_keypair().unwrap(); + let sig = DeterministicSigningKey::::sign_deterministic( + &sk, b"msg", None, + ) + .unwrap(); + assert!(matches!( + VerifyingKey::::verify( + &pk, + b"msg", + None, + &sig[..SIG_LEN - 1] + ), + Err(SignatureError::LengthError(_)) + )); + assert!(matches!( + VerifyingKey::::verify(&pk, b"msg", None, &[]), + Err(SignatureError::LengthError(_)) + )); +} + +/// One type serves both trait roles; finishing a state with the other role's `_final` is a +/// `GenericError`, not a silent wrong answer. +#[test] +fn rsa_signature_sha256_all_groups() { + let doc: Value = serde_json::from_str(&get_test_data("rsa_signature_2048_sha256_test.json")) + .expect("valid JSON"); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + let mut num_wrong_length = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + let n: [u64; 32] = limbs_from_hex(group["publicKey"]["modulus"].as_str().unwrap()); + let e = u32::from_str_radix(group["publicKey"]["publicExponent"].as_str().unwrap(), 16) + .unwrap(); + let pk = RSA2048PublicKey::new(&n, e).unwrap(); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let result = + VerifyingKey::::verify(&pk, &msg, None, &sig); + match test["result"].as_str().unwrap() { + // "acceptable" is only ever MissingNull in this file, accepted by policy (see above). + "valid" | "acceptable" => { + result.unwrap_or_else(|e| panic!("tcId {tc_id}: expected valid, got {e:?}")); + num_valid += 1; + } + "invalid" => { + assert!(result.is_err(), "tcId {tc_id}: expected invalid, got valid"); + if sig.len() != SIG_LEN { + assert!( + matches!(result, Err(SignatureError::LengthError(_))), + "tcId {tc_id}: a {}-byte signature must fail step 1's length check \ + as a LengthError, got {result:?}", + sig.len() + ); + num_wrong_length += 1; + } + num_invalid += 1; + } + other => panic!("tcId {tc_id}: unknown result {other:?}"), + } + } + } + + assert_eq!(num_tests, 259); + assert_eq!(num_valid, 10); + assert_eq!(num_invalid, 249); + assert!(num_wrong_length > 0, "the file must contain wrong-length signatures to exercise"); +} + +/// RFC 8017 §8.2.2 step 2.b: RSAVP1's "signature representative out of range" is "invalid +/// signature" through the trait (whose contract, like the RFC's, has one answer for every way a +/// signature can fail), while the generic `rsassa_pkcs1_v1_5::verify_from_hash` deliberately +/// surfaces RSAVP1's own `DecodingError` so vector-driven callers can tell malformed from wrong +/// -- see its docs. `0xff..ff` is `2^2048 - 1 >= n` for any 2048-bit `n`. +#[test] +fn trait_verify_reports_out_of_range_representative_as_invalid_signature() { + let (pk, _) = fixed_keypair().unwrap(); + let too_big = [0xffu8; SIG_LEN]; + let mut digest = [0u8; 32]; + SHA256::hash_out(b"msg", &mut digest); + assert!(matches!( + rsassa_pkcs1_v1_5::verify_from_hash::( + &pk, &digest, &too_big + ), + Err(SignatureError::DecodingError(_)) + )); + assert!(matches!( + VerifyingKey::::verify(&pk, b"msg", None, &too_big), + Err(SignatureError::SignatureVerificationFailed) + )); +} diff --git a/crypto/rsa/tests/rsa_2048_pss_sha384_sha512_tests.rs b/crypto/rsa/tests/rsa_2048_pss_sha384_sha512_tests.rs new file mode 100644 index 000000000..efdfe6834 --- /dev/null +++ b/crypto/rsa/tests/rsa_2048_pss_sha384_sha512_tests.rs @@ -0,0 +1,256 @@ +//! RSA-2048 RSASSA-PSS with SHA-384 and SHA-512, against Wycheproof's +//! `rsa_pss_2048_sha384_mgf1_48_test.json` for SHA-384. No matching-hash-and-MGF SHA-512 PSS +//! vector file exists in this Wycheproof checkout (only the SHA-512/224 and SHA-512/256 variants, +//! and a mixed SHA-512-hash/MGF1-SHA-256 "params" file, neither of which this crate's SHA-512 +//! wiring is), so SHA-512 is covered by self-consistency round trips only, the same as SHA-384's +//! sign side -- see `rsa_2048_pkcs1_v1_5_sha384_sha512_tests.rs` for why reusing the SHA-256 +//! sig-gen key here (rather than factoring a fresh one) is the right tradeoff. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{SigningKey, VerifyingKey}; +use bouncycastle_core::traits::SignatureSigner; +use bouncycastle_core_test_framework::signature::TestFrameworkSignature; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::rsa_2048::{ + RSA2048PrivateKey, RSA2048PublicKey, RSASSA_PSS_SHA384, RSASSA_PSS_SHA512, SIG_LEN, +}; +use serde_json::Value; +use std::fs; +use std::path::Path; + +/// Signs through the streaming trait path with a fixed salt (`set_salt`) -- the +/// deterministic PSS mode, for tests against a known salt. Returns `do_sign_final`'s `Result`. +macro_rules! sign_with_salt { + ($ty:ty, $sk:expr, $msg:expr, $salt:expr) => {{ + let mut signer = + SigningKey::<$ty, 256>::create_signer($sk, &mut DefaultRNG::default(), None).unwrap(); + signer.set_salt($salt); + signer.do_update($msg); + signer.do_final() + }}; +} + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +fn limbs_from_hex(hex: &str) -> [u64; L] { + let bytes_len = 8 * L; + let mut bytes = hex_decode(hex).expect("valid hex"); + if bytes.len() == bytes_len + 1 && bytes[0] == 0 { + bytes.remove(0); + } + assert_eq!(bytes.len(), bytes_len, "expected a {}-bit value", bytes_len * 8); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = bytes_len - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + limbs +} + +/// The same genuine RSA-2048 keypair used throughout this crate's other test files -- see +/// `rsa_2048_pkcs1_v1_5_tests.rs` for the full, sourced provenance. +fn genuine_key() -> RSA2048PrivateKey { + let p: [u64; 16] = [ + 0x0ea36cfb3a5b18f1, 0x48a6e65332119129, 0x110ad9e7b48a1c93, 0x569156b90113e2e9, + 0xe79813a575cfad9c, 0x69d659d143ec6f17, 0xe81e6bab5ddaa783, 0xbff1c5b80a69f788, + 0x978f6c35814f50ee, 0xe6a289ad4cfbf78f, 0x34d5681e5809d415, 0xbb028bda42eeb5d2, + 0x41c56e4de086b0d5, 0x58b8d1e24f3b55d0, 0xfb5248247d98cb7d, 0xdc431050f782e894, + ]; + let q: [u64; 16] = [ + 0x669f140cfbc20f25, 0xb97bb03677207d95, 0xfd4e06f3ed7299d4, 0x160f90536abc9492, + 0xf5b131f39098f7bc, 0xae8d72c57088d7ab, 0x89b94fbde542aba9, 0x3d3f9880ec47d5e0, + 0x1378a6868af3b7a0, 0x5544070beb057c94, 0x16611debc472fac4, 0xe500ffb79f5b8868, + 0x308a5e32196603b2, 0xea5fb19eb4eabc38, 0x122273ae3222b598, 0xbd1a81e7977f9898, + ]; + let d_p: [u64; 16] = [ + 0x209f33f09515d7c1, 0xb4a9b37656917205, 0x276933bb07e4efb9, 0x8c14019808e00414, + 0x289f96da220711e5, 0xfbbd2923d31532fe, 0xc06b414e61c0e1e7, 0x4c23c4588488961d, + 0x4dc48ae34514759c, 0x9c786961ae3e2c35, 0x497e8d9c650688e0, 0x18bf08472612dbe5, + 0x8885fb161870ee12, 0xf21d7c1479d99d47, 0x9121d91952ffd1c7, 0xa94b528b28f29159, + ]; + let d_q: [u64; 16] = [ + 0xf7597ffb68011d8d, 0x7b3cc538c4bab8c9, 0xa8fa480a81a925af, 0x6d6ede7251a383bf, + 0x8a63f788ce3a0f85, 0x0b920502eb478bc9, 0x7e37e755edfe70d9, 0x9cf9948422a16555, + 0x0d6d9ea1f2ef71fd, 0xf7efa32ea0cb6e00, 0x0629b114ca7f780f, 0xcf51176359654348, + 0x540cdcbd4ad35435, 0x31c02ff1a2bc437c, 0xff2503df78bafed5, 0x3af0e72a933aef09, + ]; + let q_inv: [u64; 16] = [ + 0x552fe4bfce945f7b, 0x67e50c999c67247b, 0xfb54ef17be3b2853, 0x241f5921b5ad3983, + 0x02de5eccd143cf31, 0x74e45f6fcc60f216, 0xafa5428a74f12708, 0x88d42294b6a2759b, + 0xe923e1097c0c562f, 0xc968b48a91c38b5b, 0x933e85179c0320b0, 0x7993d0445f758d51, + 0x9bfc042ee0924b1b, 0x41f956d90fa8a793, 0xee7a87b6483a66ee, 0x2640fbfbcfefb163, + ]; + RSA2048PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("recovered CRT components must be accepted") +} + +#[test] +fn pss_sha384_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + let salt = [0x42u8; 48]; + let sig = + sign_with_salt!(RSASSA_PSS_SHA384, &sk, b"hello", salt).expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig).expect("must verify"); + assert!(VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err()); +} + +#[test] +fn pss_sha384_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +#[test] +fn pss_sha512_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + let salt = [0x42u8; 64]; + let sig = + sign_with_salt!(RSASSA_PSS_SHA512, &sk, b"hello", salt).expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig).expect("must verify"); + assert!(VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err()); +} + +#[test] +fn pss_sha512_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +#[test] +fn rsa_pss_sha384_mgf1_48_wycheproof_vectors() { + let doc: Value = serde_json::from_str(&get_test_data("rsa_pss_2048_sha384_mgf1_48_test.json")) + .expect("valid JSON"); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-384"); + assert_eq!(group["mgfSha"], "SHA-384"); + assert_eq!(group["sLen"], 48); + let n: [u64; 32] = limbs_from_hex(group["publicKey"]["modulus"].as_str().unwrap()); + let e = u32::from_str_radix(group["publicKey"]["publicExponent"].as_str().unwrap(), 16) + .expect("publicExponent fits in u32 for every group here"); + let pk = RSA2048PublicKey::new(&n, e).expect("group public key must be valid"); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + + let Ok(sig): Result<[u8; 256], _> = sig_bytes.try_into() else { + assert_ne!(test["result"], "valid", "tcId {tc_id}: wrong-length 'valid' signature"); + num_invalid += 1; + continue; + }; + + let verified = + VerifyingKey::::verify(&pk, &msg, None, &sig).is_ok(); + match test["result"].as_str().unwrap() { + "valid" => { + assert!(verified, "tcId {tc_id}: expected valid, got invalid"); + num_valid += 1; + } + "invalid" => { + assert!(!verified, "tcId {tc_id}: expected invalid, got valid"); + num_invalid += 1; + } + other => panic!("tcId {tc_id}: unknown result {other:?}"), + } + } + } + + assert_eq!(num_tests, 141); + assert_eq!(num_valid, 95); + assert_eq!(num_invalid, 46); +} + +// ---- bouncycastle_core trait conformance ------------------------------------------------------ + +fn fixed_keypair() -> Result<(RSA2048PublicKey, RSA2048PrivateKey), SignatureError> { + let sk = genuine_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001)?; + Ok((pk, sk)) +} + +/// `core-test-framework`'s conformance suite for the SHA-384/SHA-512 PSS pairings (randomized, +/// `ctx` ignored); the bit-flip pass runs in `rsa_2048_pss_tests.rs`'s SHA-256 suite. +#[test] +fn pss_sha384_sha512_trait_conformance_suite() { + let framework = TestFrameworkSignature::new(false); + framework.test_signature::( + fixed_keypair, false, + ); + framework.test_signature::( + fixed_keypair, false, + ); +} + +#[test] +fn pss_sha384_and_sha512_reject_each_others_signatures() { + let (pk, sk) = fixed_keypair().unwrap(); + let msg = b"PSS across both APIs"; + VerifyingKey::::verify( + &pk, + msg, + None, + &SigningKey::::sign(&sk, &mut DefaultRNG::default(), msg, None) + .unwrap(), + ) + .unwrap(); + VerifyingKey::::verify( + &pk, + msg, + None, + &SigningKey::::sign(&sk, &mut DefaultRNG::default(), msg, None) + .unwrap(), + ) + .unwrap(); + let mut rng = DefaultRNG::default(); + let free_384 = SigningKey::::sign(&sk, &mut rng, msg, None).unwrap(); + let free_512 = SigningKey::::sign(&sk, &mut rng, msg, None).unwrap(); + VerifyingKey::::verify(&pk, msg, None, &free_384).unwrap(); + VerifyingKey::::verify(&pk, msg, None, &free_512).unwrap(); + // The two hash pairings are distinct encodings: neither accepts the other's signature. + assert!(VerifyingKey::::verify(&pk, msg, None, &free_512).is_err()); + assert!(VerifyingKey::::verify(&pk, msg, None, &free_384).is_err()); +} diff --git a/crypto/rsa/tests/rsa_2048_pss_shake128_tests.rs b/crypto/rsa/tests/rsa_2048_pss_shake128_tests.rs new file mode 100644 index 000000000..4fd50320a --- /dev/null +++ b/crypto/rsa/tests/rsa_2048_pss_shake128_tests.rs @@ -0,0 +1,324 @@ +//! RSA-2048 RSASSA-PSS-SHAKE128 (`id-RSASSA-PSS-SHAKE128`, RFC 8702 §3.2.1) against Wycheproof's +//! `rsa_pss_2048_shake128_test.json`, following the same split as `rsa_2048_pss_tests.rs`: no PSS +//! sig-gen vectors exist (PSS is randomized), so the sign side is a self-consistency round trip +//! against the genuine RSA-2048 key `rsa_2048_pkcs1_v1_5_tests.rs` recovered (a valid RSA private +//! key signs under any hash/MGF choice; nothing ties it to SHA-256), and the verify side is +//! checked against all of Wycheproof's real vectors. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{SigningKey, VerifyingKey}; +use bouncycastle_core::traits::RNG; +use bouncycastle_core::traits::{HashOneShot, SignatureSigner, SignatureVerifier}; +use bouncycastle_core_test_framework::signature::TestFrameworkSignature; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::rsa_2048::{ + RSA2048PrivateKey, RSA2048PublicKey, RSASSA_PSS_SHAKE128, SIG_LEN, +}; +use bouncycastle_rsa::rsassa_pss_shake; +use bouncycastle_sha3::SHAKE128; +use serde_json::Value; +use std::fs; +use std::path::Path; + +/// Signs through the streaming trait path with a fixed salt (`set_salt`) -- the +/// deterministic PSS mode, for tests against a known salt. Returns `do_sign_final`'s `Result`. +macro_rules! sign_with_salt { + ($ty:ty, $sk:expr, $msg:expr, $salt:expr) => {{ + let mut signer = + SigningKey::<$ty, 256>::create_signer($sk, &mut DefaultRNG::default(), None).unwrap(); + signer.set_salt($salt); + signer.do_update($msg); + signer.do_final() + }}; +} + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +fn limbs_from_hex(hex: &str) -> [u64; L] { + let bytes_len = 8 * L; + let mut bytes = hex_decode(hex).expect("valid hex"); + if bytes.len() == bytes_len + 1 && bytes[0] == 0 { + bytes.remove(0); + } + assert_eq!(bytes.len(), bytes_len, "expected a {}-bit value", bytes_len * 8); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = bytes_len - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + limbs +} + +/// The same genuine RSA-2048 keypair `rsa_2048_pkcs1_v1_5_tests.rs` recovers (from +/// `rsa_pkcs1_2048_sig_gen_test.json`'s SHA-256 group) -- reused here as a plain, valid RSA-2048 +/// key, not because it has anything to do with SHA-256/MGF1 specifically. +fn genuine_key() -> RSA2048PrivateKey { + let p: [u64; 16] = [ + 0x0ea36cfb3a5b18f1, 0x48a6e65332119129, 0x110ad9e7b48a1c93, 0x569156b90113e2e9, + 0xe79813a575cfad9c, 0x69d659d143ec6f17, 0xe81e6bab5ddaa783, 0xbff1c5b80a69f788, + 0x978f6c35814f50ee, 0xe6a289ad4cfbf78f, 0x34d5681e5809d415, 0xbb028bda42eeb5d2, + 0x41c56e4de086b0d5, 0x58b8d1e24f3b55d0, 0xfb5248247d98cb7d, 0xdc431050f782e894, + ]; + let q: [u64; 16] = [ + 0x669f140cfbc20f25, 0xb97bb03677207d95, 0xfd4e06f3ed7299d4, 0x160f90536abc9492, + 0xf5b131f39098f7bc, 0xae8d72c57088d7ab, 0x89b94fbde542aba9, 0x3d3f9880ec47d5e0, + 0x1378a6868af3b7a0, 0x5544070beb057c94, 0x16611debc472fac4, 0xe500ffb79f5b8868, + 0x308a5e32196603b2, 0xea5fb19eb4eabc38, 0x122273ae3222b598, 0xbd1a81e7977f9898, + ]; + let d_p: [u64; 16] = [ + 0x209f33f09515d7c1, 0xb4a9b37656917205, 0x276933bb07e4efb9, 0x8c14019808e00414, + 0x289f96da220711e5, 0xfbbd2923d31532fe, 0xc06b414e61c0e1e7, 0x4c23c4588488961d, + 0x4dc48ae34514759c, 0x9c786961ae3e2c35, 0x497e8d9c650688e0, 0x18bf08472612dbe5, + 0x8885fb161870ee12, 0xf21d7c1479d99d47, 0x9121d91952ffd1c7, 0xa94b528b28f29159, + ]; + let d_q: [u64; 16] = [ + 0xf7597ffb68011d8d, 0x7b3cc538c4bab8c9, 0xa8fa480a81a925af, 0x6d6ede7251a383bf, + 0x8a63f788ce3a0f85, 0x0b920502eb478bc9, 0x7e37e755edfe70d9, 0x9cf9948422a16555, + 0x0d6d9ea1f2ef71fd, 0xf7efa32ea0cb6e00, 0x0629b114ca7f780f, 0xcf51176359654348, + 0x540cdcbd4ad35435, 0x31c02ff1a2bc437c, 0xff2503df78bafed5, 0x3af0e72a933aef09, + ]; + let q_inv: [u64; 16] = [ + 0x552fe4bfce945f7b, 0x67e50c999c67247b, 0xfb54ef17be3b2853, 0x241f5921b5ad3983, + 0x02de5eccd143cf31, 0x74e45f6fcc60f216, 0xafa5428a74f12708, 0x88d42294b6a2759b, + 0xe923e1097c0c562f, 0xc968b48a91c38b5b, 0x933e85179c0320b0, 0x7993d0445f758d51, + 0x9bfc042ee0924b1b, 0x41f956d90fa8a793, 0xee7a87b6483a66ee, 0x2640fbfbcfefb163, + ]; + RSA2048PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("recovered CRT components must be accepted") +} + +#[test] +fn pss_shake128_sign_with_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + + let salt = [0x42u8; 32]; + let sig = sign_with_salt!(RSASSA_PSS_SHAKE128, &sk, b"the message to sign", salt) + .expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"the message to sign", None, &sig) + .expect("must verify"); + + // Deterministic given the same salt. + let sig2 = sign_with_salt!(RSASSA_PSS_SHAKE128, &sk, b"the message to sign", salt).unwrap(); + assert_eq!(sig, sig2); + + assert!( + VerifyingKey::::verify(&pk, b"a different message", None, &sig) + .is_err() + ); +} + +#[test] +fn pss_shake128_sign_with_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +#[test] +fn rsa_pss_2048_shake128_wycheproof_vectors() { + let doc: Value = serde_json::from_str(&get_test_data("rsa_pss_2048_shake128_test.json")) + .expect("valid JSON"); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHAKE128"); + assert_eq!(group["mgf"], "SHAKE128"); + assert_eq!(group["sLen"], 32); + let n: [u64; 32] = limbs_from_hex(group["publicKey"]["modulus"].as_str().unwrap()); + let e = u32::from_str_radix(group["publicKey"]["publicExponent"].as_str().unwrap(), 16) + .expect("publicExponent fits in u32 for every group here"); + let pk = RSA2048PublicKey::new(&n, e).expect("group public key must be valid"); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + + let Ok(sig): Result<[u8; 256], _> = sig_bytes.try_into() else { + assert_ne!(test["result"], "valid", "tcId {tc_id}: wrong-length 'valid' signature"); + num_invalid += 1; + continue; + }; + + let verified = + VerifyingKey::::verify(&pk, &msg, None, &sig).is_ok(); + match test["result"].as_str().unwrap() { + "valid" => { + assert!(verified, "tcId {tc_id}: expected valid, got invalid"); + num_valid += 1; + } + "invalid" => { + assert!(!verified, "tcId {tc_id}: expected invalid, got valid"); + num_invalid += 1; + } + other => panic!("tcId {tc_id}: unknown result {other:?}"), + } + } + } + + assert_eq!(num_tests, 114); + assert_eq!(num_valid, 69); + assert_eq!(num_invalid, 45); +} + +// ---- bouncycastle_core trait conformance ------------------------------------------------------ + +fn fixed_keypair() -> Result<(RSA2048PublicKey, RSA2048PrivateKey), SignatureError> { + let sk = genuine_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001)?; + Ok((pk, sk)) +} + +/// `core-test-framework`'s conformance suite for the SHAKE-native PSS path (randomized, `ctx` +/// ignored), with every signature bit flipped: `RSASSA_PSS_SHAKE` is its own generic type, so it +/// gets the exhaustive pass once, here. +#[test] +fn pss_shake128_trait_conformance_suite() { + TestFrameworkSignature::new(false) + .test_signature::( + fixed_keypair, true, + ); +} + +/// A fixed-output RNG (all-`0x42` bytes), as `bouncycastle-ecdsa`'s tests use: pins that +/// `SigningKey::sign` takes its salt from the caller's RNG and nowhere else. +struct FixedRng; +impl RNG for FixedRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0x42424242) + } + fn next_bytes(&mut self, len: usize) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0x42u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0x42); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::_256bit + } +} + +/// `set_salt` fixes the salt on the streaming path (the counterpart of ML-DSA's +/// `set_signer_rnd`): the result is deterministic and byte-identical to the generic +/// `rsassa_pss_shake::sign_from_hash_with_salt` over the same digest and salt, while an unfixed +/// streamed signature is fresh. +#[test] +fn set_salt_fixes_the_salt_on_the_streaming_path() { + let (pk, sk) = fixed_keypair().unwrap(); + let msg = b"fixed salt, streamed in two chunks"; + let salt = [0x42u8; 32]; + + let mut signer = SigningKey::::create_signer( + &sk, + &mut DefaultRNG::default(), + None, + ) + .unwrap(); + signer.set_salt(salt); + signer.do_update(&msg[..11]); + signer.do_update(&msg[11..]); + let sig = signer.do_final().unwrap(); + let mut digest = [0u8; 32]; + SHAKE128::hash_out(msg, &mut digest); + let expected = rsassa_pss_shake::sign_from_hash_with_salt::< + SHAKE128, + 32, + 32, + 72, + 223, + 32, + 64, + 65, + 16, + 32, + 33, + 256, + >(&sk, &digest, &salt) + .unwrap(); + assert_eq!(sig, expected); + VerifyingKey::::verify(&pk, msg, None, &sig).unwrap(); + + let mut fresh = SigningKey::::create_signer( + &sk, + &mut DefaultRNG::default(), + None, + ) + .unwrap(); + fresh.do_update(msg); + assert_ne!(fresh.do_final().unwrap(), sig, "without set_salt the salt is fresh"); + + // On a verify-initialised state it has no effect. + let mut verifier = + VerifyingKey::::create_verifier(&pk, None).unwrap(); + verifier.do_update(msg); + verifier.do_verify_final(&sig).unwrap(); +} + +/// `SigningKey::sign` draws the salt from the caller's RNG (the ECDSA/SM2 shape): with a +/// fixed-output RNG it equals the fixed-salt path over that same output, and with a real RNG it +/// is fresh per call and verifies. +#[test] +fn sign_takes_the_salt_from_the_callers_rng() { + let (pk, sk) = fixed_keypair().unwrap(); + let msg = b"caller-supplied RNG"; + let from_fixed_rng = + SigningKey::::sign(&sk, &mut FixedRng, msg, None).unwrap(); + assert_eq!( + from_fixed_rng, + sign_with_salt!(RSASSA_PSS_SHAKE128, &sk, msg, [0x42u8; 32]).unwrap() + ); + + let mut rng = DefaultRNG::default(); + let a = SigningKey::::sign(&sk, &mut rng, msg, None).unwrap(); + let b = SigningKey::::sign(&sk, &mut rng, msg, None).unwrap(); + assert_ne!(a, b); + VerifyingKey::::verify(&pk, msg, None, &a).unwrap(); + VerifyingKey::::verify(&pk, msg, None, &b).unwrap(); + assert!(VerifyingKey::::verify(&pk, b"other", None, &a).is_err()); +} diff --git a/crypto/rsa/tests/rsa_2048_pss_tests.rs b/crypto/rsa/tests/rsa_2048_pss_tests.rs new file mode 100644 index 000000000..fc314fef3 --- /dev/null +++ b/crypto/rsa/tests/rsa_2048_pss_tests.rs @@ -0,0 +1,318 @@ +//! RSA-2048/SHA-256/MGF1-SHA-256/32-byte-salt RSASSA-PSS against Wycheproof +//! (`rsa_pss_2048_sha256_mgf1_32_test.json`), following the same lookup convention as +//! `rsa_2048_pkcs1_v1_5_tests.rs`. +//! +//! Wycheproof carries no PSS sig-gen vectors (PSS is randomized, so there is no single "the" +//! correct signature per message the way PKCS#1 v1.5's deterministic encoding gives one): the +//! sign side is exercised here by a self-consistency round trip (sign then verify) against the +//! same genuine CRT key `rsa_2048_pkcs1_v1_5_tests.rs` recovered from +//! `rsa_pkcs1_2048_sig_gen_test.json`, and the verify side by all 108 real vectors in +//! `rsa_pss_2048_sha256_mgf1_32_test.json` -- the same public key as that sig-gen key (confirmed +//! by comparing moduli in Python before writing this file). + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{SigningKey, VerifyingKey}; +use bouncycastle_core::traits::RNG; +use bouncycastle_core::traits::{HashOneShot, SignatureSigner, SignatureVerifier}; +use bouncycastle_core_test_framework::signature::TestFrameworkSignature; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::rsa_2048::{RSA2048PrivateKey, RSA2048PublicKey, RSASSA_PSS_SHA256, SIG_LEN}; +use bouncycastle_rsa::rsassa_pss; +use bouncycastle_sha2::SHA256; +use serde_json::Value; +use std::fs; +use std::path::Path; + +/// Signs through the streaming trait path with a fixed salt (`set_salt`) -- the +/// deterministic PSS mode, for tests against a known salt. Returns `do_sign_final`'s `Result`. +macro_rules! sign_with_salt { + ($ty:ty, $sk:expr, $msg:expr, $salt:expr) => {{ + let mut signer = + SigningKey::<$ty, 256>::create_signer($sk, &mut DefaultRNG::default(), None).unwrap(); + signer.set_salt($salt); + signer.do_update($msg); + signer.do_final() + }}; +} + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +fn limbs_from_hex(hex: &str) -> [u64; L] { + let bytes_len = 8 * L; + let mut bytes = hex_decode(hex).expect("valid hex"); + if bytes.len() == bytes_len + 1 && bytes[0] == 0 { + bytes.remove(0); + } + assert_eq!(bytes.len(), bytes_len, "expected a {}-bit value", bytes_len * 8); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = bytes_len - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + limbs +} + +/// The same genuine RSA-2048 keypair `rsa_2048_pkcs1_v1_5_tests.rs` recovers from +/// `rsa_pkcs1_2048_sig_gen_test.json` -- see that file for the full, sourced provenance (p/q +/// recovered from Wycheproof's own `(n, e, d)` via the standard factoring-from-d method). +fn wycheproof_key() -> RSA2048PrivateKey { + let p: [u64; 16] = [ + 0x0ea36cfb3a5b18f1, 0x48a6e65332119129, 0x110ad9e7b48a1c93, 0x569156b90113e2e9, + 0xe79813a575cfad9c, 0x69d659d143ec6f17, 0xe81e6bab5ddaa783, 0xbff1c5b80a69f788, + 0x978f6c35814f50ee, 0xe6a289ad4cfbf78f, 0x34d5681e5809d415, 0xbb028bda42eeb5d2, + 0x41c56e4de086b0d5, 0x58b8d1e24f3b55d0, 0xfb5248247d98cb7d, 0xdc431050f782e894, + ]; + let q: [u64; 16] = [ + 0x669f140cfbc20f25, 0xb97bb03677207d95, 0xfd4e06f3ed7299d4, 0x160f90536abc9492, + 0xf5b131f39098f7bc, 0xae8d72c57088d7ab, 0x89b94fbde542aba9, 0x3d3f9880ec47d5e0, + 0x1378a6868af3b7a0, 0x5544070beb057c94, 0x16611debc472fac4, 0xe500ffb79f5b8868, + 0x308a5e32196603b2, 0xea5fb19eb4eabc38, 0x122273ae3222b598, 0xbd1a81e7977f9898, + ]; + let d_p: [u64; 16] = [ + 0x209f33f09515d7c1, 0xb4a9b37656917205, 0x276933bb07e4efb9, 0x8c14019808e00414, + 0x289f96da220711e5, 0xfbbd2923d31532fe, 0xc06b414e61c0e1e7, 0x4c23c4588488961d, + 0x4dc48ae34514759c, 0x9c786961ae3e2c35, 0x497e8d9c650688e0, 0x18bf08472612dbe5, + 0x8885fb161870ee12, 0xf21d7c1479d99d47, 0x9121d91952ffd1c7, 0xa94b528b28f29159, + ]; + let d_q: [u64; 16] = [ + 0xf7597ffb68011d8d, 0x7b3cc538c4bab8c9, 0xa8fa480a81a925af, 0x6d6ede7251a383bf, + 0x8a63f788ce3a0f85, 0x0b920502eb478bc9, 0x7e37e755edfe70d9, 0x9cf9948422a16555, + 0x0d6d9ea1f2ef71fd, 0xf7efa32ea0cb6e00, 0x0629b114ca7f780f, 0xcf51176359654348, + 0x540cdcbd4ad35435, 0x31c02ff1a2bc437c, 0xff2503df78bafed5, 0x3af0e72a933aef09, + ]; + let q_inv: [u64; 16] = [ + 0x552fe4bfce945f7b, 0x67e50c999c67247b, 0xfb54ef17be3b2853, 0x241f5921b5ad3983, + 0x02de5eccd143cf31, 0x74e45f6fcc60f216, 0xafa5428a74f12708, 0x88d42294b6a2759b, + 0xe923e1097c0c562f, 0xc968b48a91c38b5b, 0x933e85179c0320b0, 0x7993d0445f758d51, + 0x9bfc042ee0924b1b, 0x41f956d90fa8a793, 0xee7a87b6483a66ee, 0x2640fbfbcfefb163, + ]; + RSA2048PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("recovered CRT components must be accepted") +} + +#[test] +fn pss_sign_with_fixed_salt_round_trips() { + let sk = wycheproof_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + + let salt = [0x42u8; 32]; + let sig = sign_with_salt!(RSASSA_PSS_SHA256, &sk, b"the message to sign", salt) + .expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"the message to sign", None, &sig) + .expect("must verify"); + + // Deterministic given the same salt. + let sig2 = sign_with_salt!(RSASSA_PSS_SHA256, &sk, b"the message to sign", salt).unwrap(); + assert_eq!(sig, sig2); + + assert!( + VerifyingKey::::verify(&pk, b"a different message", None, &sig) + .is_err() + ); +} + +#[test] +fn pss_sign_with_rng_produces_fresh_salts_that_both_verify() { + let sk = wycheproof_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +#[test] +fn rsa_pss_sha256_mgf1_32_wycheproof_vectors() { + let doc: Value = serde_json::from_str(&get_test_data("rsa_pss_2048_sha256_mgf1_32_test.json")) + .expect("valid JSON"); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-256"); + assert_eq!(group["mgfSha"], "SHA-256"); + assert_eq!(group["sLen"], 32); + let n: [u64; 32] = limbs_from_hex(group["publicKey"]["modulus"].as_str().unwrap()); + let e = u32::from_str_radix(group["publicKey"]["publicExponent"].as_str().unwrap(), 16) + .expect("publicExponent fits in u32 for every group here"); + let pk = RSA2048PublicKey::new(&n, e).expect("group public key must be valid"); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + + let Ok(sig): Result<[u8; 256], _> = sig_bytes.try_into() else { + assert_ne!(test["result"], "valid", "tcId {tc_id}: wrong-length 'valid' signature"); + num_invalid += 1; + continue; + }; + + let verified = + VerifyingKey::::verify(&pk, &msg, None, &sig).is_ok(); + match test["result"].as_str().unwrap() { + "valid" => { + assert!(verified, "tcId {tc_id}: expected valid, got invalid"); + num_valid += 1; + } + "invalid" => { + assert!(!verified, "tcId {tc_id}: expected invalid, got valid"); + num_invalid += 1; + } + other => panic!("tcId {tc_id}: unknown result {other:?}"), + } + } + } + + assert_eq!(num_tests, 108); + assert_eq!(num_valid, 63); + assert_eq!(num_invalid, 45); +} + +// ---- bouncycastle_core trait conformance ------------------------------------------------------ + +fn fixed_keypair() -> Result<(RSA2048PublicKey, RSA2048PrivateKey), SignatureError> { + let sk = wycheproof_key(); + let pk = RSA2048PublicKey::new(sk.n(), 0x10001)?; + Ok((pk, sk)) +} + +/// `core-test-framework`'s conformance suite: randomized (`SigningKey::sign` draws a fresh salt from +/// the default RNG, so two signatures of one message differ), `ctx` ignored, every signature bit +/// flipped in turn -- run here since the generic `RSASSA_PSS` code is the same at every width. +#[test] +fn pss_sha256_trait_conformance_suite() { + TestFrameworkSignature::new(false) + .test_signature::( + fixed_keypair, true, + ); +} + +/// A fixed-output RNG (all-`0x42` bytes), as `bouncycastle-ecdsa`'s tests use: pins that +/// `SigningKey::sign` takes its salt from the caller's RNG and nowhere else. +struct FixedRng; +impl RNG for FixedRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0x42424242) + } + fn next_bytes(&mut self, len: usize) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0x42u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0x42); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::_256bit + } +} + +/// `set_salt` fixes the salt on the streaming path (the counterpart of ML-DSA's +/// `set_signer_rnd`): the result is deterministic and byte-identical to the generic +/// `rsassa_pss::sign_from_hash_with_salt` over the same digest and salt, while an unfixed +/// streamed signature is fresh. +#[test] +fn set_salt_fixes_the_salt_on_the_streaming_path() { + let (pk, sk) = fixed_keypair().unwrap(); + let msg = b"fixed salt, streamed in two chunks"; + let salt = [0x42u8; 32]; + + let mut signer = + SigningKey::::create_signer(&sk, &mut DefaultRNG::default(), None) + .unwrap(); + signer.set_salt(salt); + signer.do_update(&msg[..11]); + signer.do_update(&msg[11..]); + let sig = signer.do_final().unwrap(); + let mut digest = [0u8; 32]; + SHA256::hash_out(msg, &mut digest); + let expected = rsassa_pss::sign_from_hash_with_salt::< + SHA256, + 32, + 36, + 32, + 72, + 223, + 32, + 64, + 65, + 16, + 32, + 33, + 256, + >(&sk, &digest, &salt) + .unwrap(); + assert_eq!(sig, expected); + VerifyingKey::::verify(&pk, msg, None, &sig).unwrap(); + + let mut fresh = + SigningKey::::create_signer(&sk, &mut DefaultRNG::default(), None) + .unwrap(); + fresh.do_update(msg); + assert_ne!(fresh.do_final().unwrap(), sig, "without set_salt the salt is fresh"); + + // On a verify-initialised state it has no effect. + let mut verifier = VerifyingKey::::create_verifier(&pk, None).unwrap(); + verifier.do_update(msg); + verifier.do_verify_final(&sig).unwrap(); +} + +/// `SigningKey::sign` draws the salt from the caller's RNG (the ECDSA/SM2 shape): with a +/// fixed-output RNG it equals the fixed-salt path over that same output, and with a real RNG it +/// is fresh per call and verifies. +#[test] +fn sign_takes_the_salt_from_the_callers_rng() { + let (pk, sk) = fixed_keypair().unwrap(); + let msg = b"caller-supplied RNG"; + let from_fixed_rng = + SigningKey::::sign(&sk, &mut FixedRng, msg, None).unwrap(); + assert_eq!(from_fixed_rng, sign_with_salt!(RSASSA_PSS_SHA256, &sk, msg, [0x42u8; 32]).unwrap()); + + let mut rng = DefaultRNG::default(); + let a = SigningKey::::sign(&sk, &mut rng, msg, None).unwrap(); + let b = SigningKey::::sign(&sk, &mut rng, msg, None).unwrap(); + assert_ne!(a, b); + VerifyingKey::::verify(&pk, msg, None, &a).unwrap(); + VerifyingKey::::verify(&pk, msg, None, &b).unwrap(); + assert!(VerifyingKey::::verify(&pk, b"other", None, &a).is_err()); +} diff --git a/crypto/rsa/tests/rsa_3072_pss_shake128_tests.rs b/crypto/rsa/tests/rsa_3072_pss_shake128_tests.rs new file mode 100644 index 000000000..7c063796d --- /dev/null +++ b/crypto/rsa/tests/rsa_3072_pss_shake128_tests.rs @@ -0,0 +1,212 @@ +//! RSA-3072 RSASSA-PSS-SHAKE128 (RFC 8702 §3.2.1) against Wycheproof's +//! `rsa_pss_3072_shake128_test.json`, following `rsa_2048_pss_shake128_tests.rs`'s split: a +//! self-consistency round trip for signing (reusing the genuine RSA-3072 key +//! `rsa_3072_tests.rs` recovered), and all of Wycheproof's real vectors for verification. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{SigningKey, VerifyingKey}; +use bouncycastle_core::traits::SignatureSigner; +use bouncycastle_core_test_framework::signature::TestFrameworkSignature; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::rsa_3072::{ + RSA3072PrivateKey, RSA3072PublicKey, RSASSA_PSS_SHAKE128, SIG_LEN, +}; +use serde_json::Value; +use std::fs; +use std::path::Path; + +/// Signs through the streaming trait path with a fixed salt (`set_salt`) -- the +/// deterministic PSS mode, for tests against a known salt. Returns `do_sign_final`'s `Result`. +macro_rules! sign_with_salt { + ($ty:ty, $sk:expr, $msg:expr, $salt:expr) => {{ + let mut signer = + SigningKey::<$ty, 384>::create_signer($sk, &mut DefaultRNG::default(), None).unwrap(); + signer.set_salt($salt); + signer.do_update($msg); + signer.do_final() + }}; +} + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +fn limbs_from_hex(hex: &str) -> [u64; L] { + let bytes_len = 8 * L; + let mut bytes = hex_decode(hex).expect("valid hex"); + if bytes.len() == bytes_len + 1 && bytes[0] == 0 { + bytes.remove(0); + } + assert_eq!(bytes.len(), bytes_len, "expected a {}-bit value", bytes_len * 8); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = bytes_len - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + limbs +} + +/// The same genuine RSA-3072 keypair `rsa_3072_tests.rs` recovers (from +/// `rsa_pkcs1_3072_sig_gen_test.json`'s SHA-256 group) -- reused here as a plain, valid RSA-3072 +/// key, not because it has anything to do with SHA-256/MGF1 specifically. +fn genuine_key() -> RSA3072PrivateKey { + let p: [u64; 24] = [ + 0xb26d973345bc4c5f, 0x23251a1d29962ca9, 0x554fc3f23d6c9046, 0x89ffe73b1401e9b8, + 0xe0b448b454670aca, 0xa73ea5c2413d1da2, 0xe9539bc7a8d3b351, 0x357984fc116af9cb, + 0xa93e40aac908e4e3, 0xae586c1e7f5c52cd, 0x80deaeaab651c7a9, 0xe261a4f7f4505580, + 0xc5a703b2fc28bfcf, 0x14521a6893f3f3c5, 0x0f08d649107f449a, 0xd96ceabb7ee83ce5, + 0x1b2e8a9b2bb69525, 0x383ead851fac07ad, 0x939deb88dff68550, 0xce9fa16a1cc92120, + 0x43604a7f2be2860f, 0xba55f20a964c4e63, 0x0ed9ac8a812545da, 0xf5eca16e0e83696b, + ]; + let q: [u64; 24] = [ + 0x7981610848c55cdd, 0x8bce03c5d339b975, 0xb8227d7b5d76ce8b, 0x6edee1e9d17781db, + 0x1c127ac3c4d0bd59, 0xb8b28f38951d7bee, 0x496086fa1300249a, 0x790e5c34729a8efb, + 0x9bdb81968f4a6d7c, 0x6018fd4ed9a3545f, 0x6ccb168d5b510dbe, 0xe1d845c6553c0a54, + 0x73ec1d97f669e298, 0x8b898025ef470e43, 0x160deb327e8ace01, 0x640191099b355611, + 0xe0ec501ccc94b2b0, 0x4f98c9ee4fdae41b, 0x0082a3bc42af1a14, 0x510a623c2b47a522, + 0x32057f9da6dbefc4, 0x95ad92b6f295d610, 0x19ddbfcfa2d96704, 0xcf25446f59cf5129, + ]; + let d_p: [u64; 24] = [ + 0xeeb10a8531c470ed, 0xbcd9be04cdc9d65c, 0xb684b458e4ab3854, 0x6a2dafd0d3b23a21, + 0xc812cbd3dccc8b35, 0x3e67363a947405c6, 0xa3ce9c7d391bdbb2, 0xf8bd10156b4bd580, + 0x3c1ce3ae99eb37da, 0xcfa5f4771567cc23, 0x35e0be9a437021c1, 0x8a527b7b967be52e, + 0xc649435b483585d6, 0x1dc154ddadf6bc20, 0x69105ecfc1144838, 0x3dad9bdd05d4f6d4, + 0xf95601b3d122be79, 0x92f1eed27a0ada46, 0x152e93f904cfe6e6, 0x5a6e6d9c19e8bdb3, + 0x6c0437d3cb7c843f, 0x370e84e9f5f0f931, 0x514c6940c20eb67b, 0x6357a59679d26801, + ]; + let d_q: [u64; 24] = [ + 0x1a4dbeba87772d29, 0xe51861bbb4e73e73, 0x9e86b2483e9bf22c, 0xc9ebcf4d7c2d6c9f, + 0xbe8fe9def6b7a62c, 0x4e8805a755ac2904, 0x734f44f3e4e88d18, 0x7626602fc90ae694, + 0xde97c88d40fa1ac4, 0x4eeaf1aafe2e1ba5, 0x25cd66f5205b038b, 0x8fa6bb6ae73b182d, + 0x0c85ab03068cbfee, 0x96f0ae48529b490f, 0xbb4cc906ba283d18, 0x4bf60c135a264919, + 0x4c2cf95ce74fe42c, 0xf4a5e8eed2a70c7a, 0x14ecee22e846a7d3, 0xd2a4139a39cec9df, + 0x4ba0e0801d31cbf5, 0x088a7986f6c2b8c0, 0x8bdc0f566f876191, 0x0004dadabfc15b1a, + ]; + let q_inv: [u64; 24] = [ + 0x45b268741fca195c, 0x94eacf955ae5dacd, 0x3ad1a334fd85aa87, 0xe8b01c83f2aec93e, + 0xd05fdc78929fa0cf, 0x0745ab95edda215b, 0xe5c81a2189f11467, 0xeded3f90e0ba4a97, + 0xfbf69baed9510be5, 0x6bee976c25dbaff8, 0x6ad65e9fe6c28038, 0xef6613dd6bf885c1, + 0x594990da705ebdf7, 0x8ee8b716f19429fb, 0x2662db8c85318de4, 0x22481fc74a7a3d62, + 0xfe6c3600d9b8e9a9, 0x744fac4daabf5488, 0xfbee6ec24b75fbf0, 0x8be552c8be44f139, + 0xfb0da96bd423759d, 0xb3443d93e7e8ca62, 0x36fe01b950885ecd, 0x214a1f73130e48b3, + ]; + RSA3072PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("recovered CRT components must be accepted") +} + +#[test] +fn pss_shake128_sign_with_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA3072PublicKey::new(sk.n(), 0x10001).unwrap(); + + let salt = [0x42u8; 32]; + let sig = sign_with_salt!(RSASSA_PSS_SHAKE128, &sk, b"the message to sign", salt) + .expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"the message to sign", None, &sig) + .expect("must verify"); + + let sig2 = sign_with_salt!(RSASSA_PSS_SHAKE128, &sk, b"the message to sign", salt).unwrap(); + assert_eq!(sig, sig2); + + assert!( + VerifyingKey::::verify(&pk, b"a different message", None, &sig) + .is_err() + ); +} + +#[test] +fn pss_shake128_sign_with_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA3072PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +#[test] +fn rsa_pss_3072_shake128_wycheproof_vectors() { + let doc: Value = serde_json::from_str(&get_test_data("rsa_pss_3072_shake128_test.json")) + .expect("valid JSON"); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHAKE128"); + assert_eq!(group["mgf"], "SHAKE128"); + assert_eq!(group["sLen"], 32); + let n: [u64; 48] = limbs_from_hex(group["publicKey"]["modulus"].as_str().unwrap()); + let e = u32::from_str_radix(group["publicKey"]["publicExponent"].as_str().unwrap(), 16) + .expect("publicExponent fits in u32 for every group here"); + let pk = RSA3072PublicKey::new(&n, e).expect("group public key must be valid"); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + + let Ok(sig): Result<[u8; 384], _> = sig_bytes.try_into() else { + assert_ne!(test["result"], "valid", "tcId {tc_id}: wrong-length 'valid' signature"); + num_invalid += 1; + continue; + }; + + let verified = + VerifyingKey::::verify(&pk, &msg, None, &sig).is_ok(); + match test["result"].as_str().unwrap() { + "valid" => { + assert!(verified, "tcId {tc_id}: expected valid, got invalid"); + num_valid += 1; + } + "invalid" => { + assert!(!verified, "tcId {tc_id}: expected invalid, got valid"); + num_invalid += 1; + } + other => panic!("tcId {tc_id}: unknown result {other:?}"), + } + } + } + + assert_eq!(num_tests, 114); + assert_eq!(num_valid, 69); + assert_eq!(num_invalid, 45); +} + +// ---- bouncycastle_core trait conformance ------------------------------------------------------ + +fn fixed_keypair() -> Result<(RSA3072PublicKey, RSA3072PrivateKey), SignatureError> { + let sk = genuine_key(); + let pk = RSA3072PublicKey::new(sk.n(), 0x10001)?; + Ok((pk, sk)) +} + +#[test] +fn pss_shake128_trait_conformance_suite() { + TestFrameworkSignature::new(false) + .test_signature::( + fixed_keypair, false, + ); +} diff --git a/crypto/rsa/tests/rsa_3072_tests.rs b/crypto/rsa/tests/rsa_3072_tests.rs new file mode 100644 index 000000000..8afc28286 --- /dev/null +++ b/crypto/rsa/tests/rsa_3072_tests.rs @@ -0,0 +1,433 @@ +//! RSA-3072 against Wycheproof: `rsa_pkcs1_3072_sig_gen_test.json` (sign), `rsa_signature_3072_ +//! sha{256,384,512}_test.json` (verify, all three hashes), and `rsa_pss_3072_sha256_mgf1_32_test.json` +//! (PSS verify; no matching-hash-and-MGF PSS vector file exists for SHA-384/512 at this size, so +//! those are self-consistency only, following `rsa_2048_pss_sha384_sha512_tests.rs`'s precedent -- +//! including its fixed-salt-round-trip-plus-rejection and RNG-freshness test shape, so every +//! PSS signing path (three hashes, RNG-drawn or fixed salt) is actually exercised at least once). +//! +//! `p`/`q`/`dP`/`dQ`/`qInv` were recovered from `rsa_pkcs1_3072_sig_gen_test.json`'s SHA-256 +//! group's `(n, e, d)` via the same factoring-from-d method as RSA-2048's key (see +//! `rsa_2048_pkcs1_v1_5_tests.rs`), cross-checked against all 8 of that group's real signatures +//! before being pasted here. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::SignatureSigner; +use bouncycastle_core_test_framework::signature::{ + TestFrameworkSignature, TestFrameworkSignatureKeys, +}; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::rsa_3072::{ + PK_LEN, RSA3072PrivateKey, RSA3072PublicKey, RSASSA_PKCS1_v1_5_SHA256, + RSASSA_PKCS1_v1_5_SHA384, RSASSA_PKCS1_v1_5_SHA512, RSASSA_PSS_SHA256, RSASSA_PSS_SHA384, + RSASSA_PSS_SHA512, SIG_LEN, SK_LEN, +}; +use serde_json::Value; +use std::fs; +use std::path::Path; + +/// Signs through the streaming trait path with a fixed salt (`set_salt`) -- the +/// deterministic PSS mode, for tests against a known salt. Returns `do_sign_final`'s `Result`. +macro_rules! sign_with_salt { + ($ty:ty, $sk:expr, $msg:expr, $salt:expr) => {{ + let mut signer = + SigningKey::<$ty, 384>::create_signer($sk, &mut DefaultRNG::default(), None).unwrap(); + signer.set_salt($salt); + signer.do_update($msg); + signer.do_final() + }}; +} + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +fn limbs_from_hex(hex: &str) -> [u64; L] { + let bytes_len = 8 * L; + let mut bytes = hex_decode(hex).expect("valid hex"); + if bytes.len() == bytes_len + 1 && bytes[0] == 0 { + bytes.remove(0); + } + assert_eq!(bytes.len(), bytes_len, "expected a {}-bit value", bytes_len * 8); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = bytes_len - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + limbs +} + +fn genuine_key() -> RSA3072PrivateKey { + let p: [u64; 24] = [ + 0xb26d973345bc4c5f, 0x23251a1d29962ca9, 0x554fc3f23d6c9046, 0x89ffe73b1401e9b8, + 0xe0b448b454670aca, 0xa73ea5c2413d1da2, 0xe9539bc7a8d3b351, 0x357984fc116af9cb, + 0xa93e40aac908e4e3, 0xae586c1e7f5c52cd, 0x80deaeaab651c7a9, 0xe261a4f7f4505580, + 0xc5a703b2fc28bfcf, 0x14521a6893f3f3c5, 0x0f08d649107f449a, 0xd96ceabb7ee83ce5, + 0x1b2e8a9b2bb69525, 0x383ead851fac07ad, 0x939deb88dff68550, 0xce9fa16a1cc92120, + 0x43604a7f2be2860f, 0xba55f20a964c4e63, 0x0ed9ac8a812545da, 0xf5eca16e0e83696b, + ]; + let q: [u64; 24] = [ + 0x7981610848c55cdd, 0x8bce03c5d339b975, 0xb8227d7b5d76ce8b, 0x6edee1e9d17781db, + 0x1c127ac3c4d0bd59, 0xb8b28f38951d7bee, 0x496086fa1300249a, 0x790e5c34729a8efb, + 0x9bdb81968f4a6d7c, 0x6018fd4ed9a3545f, 0x6ccb168d5b510dbe, 0xe1d845c6553c0a54, + 0x73ec1d97f669e298, 0x8b898025ef470e43, 0x160deb327e8ace01, 0x640191099b355611, + 0xe0ec501ccc94b2b0, 0x4f98c9ee4fdae41b, 0x0082a3bc42af1a14, 0x510a623c2b47a522, + 0x32057f9da6dbefc4, 0x95ad92b6f295d610, 0x19ddbfcfa2d96704, 0xcf25446f59cf5129, + ]; + let d_p: [u64; 24] = [ + 0xeeb10a8531c470ed, 0xbcd9be04cdc9d65c, 0xb684b458e4ab3854, 0x6a2dafd0d3b23a21, + 0xc812cbd3dccc8b35, 0x3e67363a947405c6, 0xa3ce9c7d391bdbb2, 0xf8bd10156b4bd580, + 0x3c1ce3ae99eb37da, 0xcfa5f4771567cc23, 0x35e0be9a437021c1, 0x8a527b7b967be52e, + 0xc649435b483585d6, 0x1dc154ddadf6bc20, 0x69105ecfc1144838, 0x3dad9bdd05d4f6d4, + 0xf95601b3d122be79, 0x92f1eed27a0ada46, 0x152e93f904cfe6e6, 0x5a6e6d9c19e8bdb3, + 0x6c0437d3cb7c843f, 0x370e84e9f5f0f931, 0x514c6940c20eb67b, 0x6357a59679d26801, + ]; + let d_q: [u64; 24] = [ + 0x1a4dbeba87772d29, 0xe51861bbb4e73e73, 0x9e86b2483e9bf22c, 0xc9ebcf4d7c2d6c9f, + 0xbe8fe9def6b7a62c, 0x4e8805a755ac2904, 0x734f44f3e4e88d18, 0x7626602fc90ae694, + 0xde97c88d40fa1ac4, 0x4eeaf1aafe2e1ba5, 0x25cd66f5205b038b, 0x8fa6bb6ae73b182d, + 0x0c85ab03068cbfee, 0x96f0ae48529b490f, 0xbb4cc906ba283d18, 0x4bf60c135a264919, + 0x4c2cf95ce74fe42c, 0xf4a5e8eed2a70c7a, 0x14ecee22e846a7d3, 0xd2a4139a39cec9df, + 0x4ba0e0801d31cbf5, 0x088a7986f6c2b8c0, 0x8bdc0f566f876191, 0x0004dadabfc15b1a, + ]; + let q_inv: [u64; 24] = [ + 0x45b268741fca195c, 0x94eacf955ae5dacd, 0x3ad1a334fd85aa87, 0xe8b01c83f2aec93e, + 0xd05fdc78929fa0cf, 0x0745ab95edda215b, 0xe5c81a2189f11467, 0xeded3f90e0ba4a97, + 0xfbf69baed9510be5, 0x6bee976c25dbaff8, 0x6ad65e9fe6c28038, 0xef6613dd6bf885c1, + 0x594990da705ebdf7, 0x8ee8b716f19429fb, 0x2662db8c85318de4, 0x22481fc74a7a3d62, + 0xfe6c3600d9b8e9a9, 0x744fac4daabf5488, 0xfbee6ec24b75fbf0, 0x8be552c8be44f139, + 0xfb0da96bd423759d, 0xb3443d93e7e8ca62, 0x36fe01b950885ecd, 0x214a1f73130e48b3, + ]; + RSA3072PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("recovered CRT components must be accepted") +} + +#[test] +fn pkcs1_v1_5_sig_gen_3072_sha256() { + let sk = genuine_key(); + let doc: Value = + serde_json::from_str(&get_test_data("rsa_pkcs1_3072_sig_gen_test.json")).unwrap(); + let group = doc["testGroups"] + .as_array() + .unwrap() + .iter() + .find(|g| g["sha"] == "SHA-256" && g["tests"].as_array().unwrap().len() == 8) + .expect("the main SHA-256/3072 group"); + + let mut num_tests = 0usize; + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let expected_sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let sig = DeterministicSigningKey::::sign_deterministic( + &sk, &msg, None, + ) + .unwrap_or_else(|e| { + panic!("tcId {tc_id}: signing failed: {e:?}"); + }); + assert_eq!(sig.to_vec(), expected_sig, "tcId {tc_id}: signature mismatch"); + } + assert_eq!(num_tests, 8); +} + +#[test] +fn pkcs1_v1_5_sha384_and_sha512_round_trip() { + let sk = genuine_key(); + let pk = RSA3072PublicKey::new(sk.n(), 0x10001).unwrap(); + let sig384 = DeterministicSigningKey::::sign_deterministic( + &sk, b"hello", None, + ) + .unwrap(); + VerifyingKey::::verify(&pk, b"hello", None, &sig384).unwrap(); + let sig512 = DeterministicSigningKey::::sign_deterministic( + &sk, b"hello", None, + ) + .unwrap(); + VerifyingKey::::verify(&pk, b"hello", None, &sig512).unwrap(); +} + +/// Mutation testing found that fixed-salt PSS/SHA-256 signing, RNG-salted PSS/SHA-384 signing, +/// fixed-salt PSS/SHA-512 signing, and PSS/SHA-384 and /SHA-512 verification's rejection paths were +/// never exercised at this modulus size (only the RNG or `_with_salt` half of each hash was +/// called, and no test asserted a wrong message is rejected for SHA-384/512): a +/// whole-function-body mutant replacing any of them with a constant still passed the whole suite. +/// Split into six tests, one per (hash, salt source) pairing, matching +/// `rsa_2048_pss_sha384_sha512_tests.rs`'s own shape so every function is called and every +/// rejection path is checked. +#[test] +fn pss_sha256_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA3072PublicKey::new(sk.n(), 0x10001).unwrap(); + let salt = [0x22u8; 32]; + let sig = + sign_with_salt!(RSASSA_PSS_SHA256, &sk, b"hello", salt).expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig).expect("must verify"); + assert!(VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err()); +} + +#[test] +fn pss_sha256_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA3072PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +#[test] +fn pss_sha384_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA3072PublicKey::new(sk.n(), 0x10001).unwrap(); + let salt = [0x11u8; 48]; + let sig = + sign_with_salt!(RSASSA_PSS_SHA384, &sk, b"hello", salt).expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig).expect("must verify"); + assert!(VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err()); +} + +#[test] +fn pss_sha384_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA3072PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +#[test] +fn pss_sha512_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA3072PublicKey::new(sk.n(), 0x10001).unwrap(); + let salt = [0x33u8; 64]; + let sig = + sign_with_salt!(RSASSA_PSS_SHA512, &sk, b"hello", salt).expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig).expect("must verify"); + assert!(VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err()); +} + +#[test] +fn pss_sha512_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA3072PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +fn run_pkcs1_v1_5_verify_vectors( + filename: &str, + verify: impl Fn(&RSA3072PublicKey, &[u8], &[u8; 384]) -> bool, + expected_sha: &str, + expected_valid: usize, + expected_invalid: usize, +) { + let doc: Value = serde_json::from_str(&get_test_data(filename)).expect("valid JSON"); + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + let mut num_missing_null = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], expected_sha); + let n: [u64; 48] = limbs_from_hex(group["publicKey"]["modulus"].as_str().unwrap()); + let e = u32::from_str_radix(group["publicKey"]["publicExponent"].as_str().unwrap(), 16) + .unwrap(); + let pk = RSA3072PublicKey::new(&n, e).unwrap(); + + for test in group["tests"].as_array().unwrap() { + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let flags: Vec<&str> = + test["flags"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect(); + + let Ok(sig): Result<[u8; 384], _> = sig_bytes.try_into() else { + assert_ne!(test["result"], "valid"); + num_invalid += 1; + continue; + }; + let verified = verify(&pk, &msg, &sig); + match test["result"].as_str().unwrap() { + "valid" => { + assert!(verified, "tcId {tc_id}: expected valid, got invalid"); + num_valid += 1; + } + "invalid" => { + assert!(!verified, "tcId {tc_id}: expected invalid, got valid"); + num_invalid += 1; + } + "acceptable" => { + assert_eq!(flags, vec!["MissingNull"], "tcId {tc_id}: unreviewed acceptable"); + assert!(verified); + num_missing_null += 1; + } + other => panic!("tcId {tc_id}: unknown result {other:?}"), + } + } + } + assert_eq!(num_missing_null, 1); + assert_eq!(num_valid, expected_valid, "for {filename}"); + assert_eq!(num_invalid, expected_invalid, "for {filename}"); +} + +#[test] +fn rsa_signature_3072_sha256_wycheproof_vectors() { + run_pkcs1_v1_5_verify_vectors( + "rsa_signature_3072_sha256_test.json", + |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }, + "SHA-256", + 8, + 250, + ); +} + +/// Mutation testing found PKCS#1 v1.5/SHA-384 verification had no rejection-path coverage at this +/// modulus size (only SHA-256's real vector file was wired up, despite this file's own module +/// docs already claiming all three hashes): a whole-function-body mutant that always returned +/// `Ok(())` still passed the whole suite. +#[test] +fn rsa_signature_3072_sha384_wycheproof_vectors() { + run_pkcs1_v1_5_verify_vectors( + "rsa_signature_3072_sha384_test.json", + |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }, + "SHA-384", + 7, + 251, + ); +} + +/// As [`rsa_signature_3072_sha384_wycheproof_vectors`], for PKCS#1 v1.5/SHA-512. This +/// file's `rsa_signature_3072_sha512_test.json` has a second, one-test group (a `SmallSignature`/ +/// `SmallPublicKey` edge case under its own key), which `run_pkcs1_v1_5_verify_vectors` already +/// handles by deriving `pk` fresh per group -- hence one more `valid` than SHA-384's file. +#[test] +fn rsa_signature_3072_sha512_wycheproof_vectors() { + run_pkcs1_v1_5_verify_vectors( + "rsa_signature_3072_sha512_test.json", + |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }, + "SHA-512", + 8, + 251, + ); +} + +#[test] +fn rsa_pss_3072_sha256_mgf1_32_wycheproof_vectors() { + let doc: Value = + serde_json::from_str(&get_test_data("rsa_pss_3072_sha256_mgf1_32_test.json")).unwrap(); + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHA-256"); + assert_eq!(group["sLen"], 32); + let n: [u64; 48] = limbs_from_hex(group["publicKey"]["modulus"].as_str().unwrap()); + let e = u32::from_str_radix(group["publicKey"]["publicExponent"].as_str().unwrap(), 16) + .unwrap(); + let pk = RSA3072PublicKey::new(&n, e).unwrap(); + + for test in group["tests"].as_array().unwrap() { + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let Ok(sig): Result<[u8; 384], _> = sig_bytes.try_into() else { + assert_ne!(test["result"], "valid"); + num_invalid += 1; + continue; + }; + let verified = + VerifyingKey::::verify(&pk, &msg, None, &sig).is_ok(); + match test["result"].as_str().unwrap() { + "valid" => { + assert!(verified, "tcId {tc_id}: expected valid"); + num_valid += 1; + } + "invalid" => { + assert!(!verified, "tcId {tc_id}: expected invalid"); + num_invalid += 1; + } + other => panic!("unknown result {other:?}"), + } + } + } + assert_eq!(num_valid, 63); + assert_eq!(num_invalid, 45); +} + +// ---- bouncycastle_core trait conformance ------------------------------------------------------ + +fn fixed_keypair() -> Result<(RSA3072PublicKey, RSA3072PrivateKey), SignatureError> { + let sk = genuine_key(); + let pk = RSA3072PublicKey::new(sk.n(), 0x10001)?; + Ok((pk, sk)) +} + +/// `core-test-framework`'s conformance suite for every RSA-3072 pairing (the exhaustive bit-flip +/// pass over the shared generic code runs at RSA-2048). +#[test] +fn pkcs1_v1_5_trait_conformance_suites() { + let framework = TestFrameworkSignature::new(false); + framework.test_deterministic_signature::(fixed_keypair, false); + framework.test_deterministic_signature::(fixed_keypair, false); + framework.test_deterministic_signature::(fixed_keypair, false); +} + +#[test] +fn pss_trait_conformance_suites() { + let framework = TestFrameworkSignature::new(false); + framework.test_signature::( + fixed_keypair, false, + ); + framework.test_signature::( + fixed_keypair, false, + ); + framework.test_signature::( + fixed_keypair, false, + ); +} + +#[test] +fn key_trait_boundary_conditions() { + TestFrameworkSignatureKeys::new() + .test_keys::(fixed_keypair); +} diff --git a/crypto/rsa/tests/rsa_4096_pss_shake256_tests.rs b/crypto/rsa/tests/rsa_4096_pss_shake256_tests.rs new file mode 100644 index 000000000..e31c0bc91 --- /dev/null +++ b/crypto/rsa/tests/rsa_4096_pss_shake256_tests.rs @@ -0,0 +1,226 @@ +//! RSA-4096 RSASSA-PSS-SHAKE256 (RFC 8702 §3.2.1) against Wycheproof's +//! `rsa_pss_4096_shake256_test.json`, following `rsa_2048_pss_shake128_tests.rs`'s split: a +//! self-consistency round trip for signing (reusing the genuine RSA-4096 key `rsa_4096_tests.rs` +//! recovered), and all of Wycheproof's real vectors for verification. + +use bouncycastle_core::signature_key::{SigningKey, VerifyingKey}; +use bouncycastle_core::traits::SignatureSigner; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::rsa_4096::{RSA4096PrivateKey, RSA4096PublicKey, RSASSA_PSS_SHAKE256}; +use serde_json::Value; +use std::fs; +use std::path::Path; + +/// Signs through the streaming trait path with a fixed salt (`set_salt`) -- the +/// deterministic PSS mode, for tests against a known salt. Returns `do_sign_final`'s `Result`. +macro_rules! sign_with_salt { + ($ty:ty, $sk:expr, $msg:expr, $salt:expr) => {{ + let mut signer = + SigningKey::<$ty, 512>::create_signer($sk, &mut DefaultRNG::default(), None).unwrap(); + signer.set_salt($salt); + signer.do_update($msg); + signer.do_final() + }}; +} + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +fn limbs_from_hex(hex: &str) -> [u64; L] { + let bytes_len = 8 * L; + let mut bytes = hex_decode(hex).expect("valid hex"); + if bytes.len() == bytes_len + 1 && bytes[0] == 0 { + bytes.remove(0); + } + assert_eq!(bytes.len(), bytes_len, "expected a {}-bit value", bytes_len * 8); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = bytes_len - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + limbs +} + +/// The same genuine RSA-4096 keypair `rsa_4096_tests.rs` recovers (from +/// `rsa_pkcs1_4096_sig_gen_test.json`'s SHA-256 group) -- reused here as a plain, valid RSA-4096 +/// key, not because it has anything to do with SHA-256/MGF1 specifically. +fn genuine_key() -> RSA4096PrivateKey { + let p: [u64; 32] = [ + 0x5141acd4afd4771f, 0x5857742b7e4032dc, 0xb7a9b97fe5371396, 0x4b1783c5bc91a6dc, + 0xaf5d0c912d97b728, 0xeb265a7f28b88976, 0x5e993302aa72f11d, 0xc007ad09d76ae22a, + 0x44cbb87b8bec8fd3, 0xf152f15c73b95ac5, 0x8dbdad655fbbad20, 0x59855d0de1c794ac, + 0x4c6060926d878a20, 0xf38860474e44993d, 0x177a91caeef98b13, 0xd1676123544d3def, + 0xc1673063d8487390, 0xfedd55ba2e233a6d, 0x016a5735b883c639, 0xbd813f9fb752805b, + 0x923a7b775b5ffaff, 0xb174e55ca6a12c21, 0x0373077895a6934a, 0x6b725388f54ffea3, + 0x953c09104e1fd2d3, 0x9114f39017871528, 0x33918eb72b786b3c, 0xf43b8626af13d14a, + 0x886a0904eb486140, 0x0037409b81f3a5b3, 0x19e44e26140d2ede, 0xc3c677495c2bd566, + ]; + let q: [u64; 32] = [ + 0xee0745546a6a1c73, 0x8b1050436d882425, 0xdc2e7872ce6fb2b7, 0xf98671ee4899050b, + 0x796a5dd847400804, 0x9ba1f51132355792, 0xc39066a9fc27985b, 0x81f4bca5d15e0962, + 0x6a46cf6dbb6d2ad9, 0xdf3d085ae7693b7e, 0xf58e3c750b6d978a, 0xb52b9000d3a7aab1, + 0xdbb07e23bce1960c, 0x4e291b5c8ddaa6a7, 0x0544a7a7cb3f0792, 0xc695e324fbb11beb, + 0xbded418507f2f175, 0x49fe5f47737ff436, 0xeb37f85b7554d7ad, 0x724d042aef25cea2, + 0xeadbd11dcd2fec7b, 0xb50812705b379f5d, 0xfc50fa98fb67b746, 0x16f2bdf5964b26c7, + 0xcc1c817507859246, 0x6810c2136ad81b16, 0xd7bc78d637e17e0e, 0x6882825347455782, + 0xcdb615d921f5e705, 0x0a982efbe6658e6b, 0x59e96686f7b8752f, 0xc357cf685f9b8d4e, + ]; + let d_p: [u64; 32] = [ + 0x3cd8e91fee6bcdd3, 0x074fdc015ab1c45f, 0x34135987a2288907, 0xf6961d44dfb7505c, + 0x251be32315a85b75, 0xe8982e8493a2ae78, 0xc268493861002977, 0x140fb5806735814e, + 0x35d426d719b12c41, 0xa5f194dbbf374226, 0xa0a6c5314c1a2375, 0x66899d59c04b60c4, + 0x5158d7867dbfec94, 0xec1dde997c00125e, 0x62e5a19b2a71decf, 0x310291892c37e8a2, + 0x223e06c9521f4a45, 0xe4f08e1d5543ed74, 0xcf4dc8de58b9e9ad, 0x9167ad4f122b3263, + 0xf110dbac286712f6, 0x2db237d0f39cda5f, 0xa54b67367c857476, 0x138b37b7b1ed2219, + 0x950d9f4e82aab9bc, 0x90943e671e928e23, 0x44791809c9fcf19f, 0xc70396da054ba478, + 0x77248f6fe00e31bd, 0xf3731b1415d5f2ee, 0xf5242aa6657760cd, 0xa6bbb5460638d2b2, + ]; + let d_q: [u64; 32] = [ + 0xfa935bd36284ea6b, 0x9c255a57d6b3ac0f, 0x35b350881ec52e99, 0xf48d35dd11598957, + 0x61fee5d3763b8cfb, 0xf86f5d1936a161ae, 0x3a8b852507264ae2, 0xe558f90950a572a2, + 0x205aeb15029c95a6, 0x0fa8cf020e960c3b, 0x6a793c66abfb0963, 0xa9c7d7b391d0cbee, + 0x6310f31421e85c6b, 0xc0de89d2edb32796, 0x1cf37af6eab4ed97, 0x1533eacf819c878e, + 0xc700fc7e2a482abe, 0x4d1c62a63d917682, 0x89141558095c3228, 0x029bb05df0b8f121, + 0xa239e00b24ba9b5b, 0x74c3c55d5051f35f, 0x2f45200123860c7d, 0x4fabf224d27abaf1, + 0x0acb80fbf7ed2d86, 0x87db455a8bee03ce, 0x980bbbd1afa8fac6, 0x9d9ee6515ded4930, + 0x4c62a05dae0f744b, 0x69ff90e2839348c9, 0xda141e6d351e42da, 0x178cd58f72bf5118, + ]; + let q_inv: [u64; 32] = [ + 0x5792505b857d3e8d, 0x8a13922b1240c0d2, 0xb5fcb8bf3df1f936, 0x74483606c2cd7ed8, + 0xd40f33bbfd9be093, 0xb5f40ed6632989f5, 0x7f2cc0cd8c8a0661, 0x87630c6b25dddeb5, + 0x7032a5af6ef7c20f, 0x90331600142e6cb9, 0x67b92645b68c6d8f, 0xb63517c7c39843cf, + 0xd50712bf8fc35882, 0x3c421e2447b1a989, 0x7a251afa7aaa6908, 0x43b4de11f99b2654, + 0x1e02735920ef48a3, 0x218fc24da34ce8f0, 0x280446e49b3b9b15, 0x8d96f07411c882bf, + 0x66e283f5d5fb0e6e, 0x0844efd28d853446, 0x5eb4def8dba57b99, 0x474111f92d19f3ab, + 0x748d7b5d94cacd4d, 0xcf634f3a07cea4b7, 0x8dbcca51f4da4379, 0x80774236a54ec9dc, + 0x6a1ef00ee582d3d1, 0xdecfb14ca1e80c8e, 0xc78af5f6c807cc99, 0x484ad86e79415ea3, + ]; + RSA4096PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("recovered CRT components must be accepted") +} + +#[test] +fn pss_shake256_sign_with_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA4096PublicKey::new(sk.n(), 0x10001).unwrap(); + + let salt = [0x42u8; 64]; + let sig = sign_with_salt!(RSASSA_PSS_SHAKE256, &sk, b"the message to sign", salt) + .expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"the message to sign", None, &sig) + .expect("must verify"); + + let sig2 = sign_with_salt!(RSASSA_PSS_SHAKE256, &sk, b"the message to sign", salt).unwrap(); + assert_eq!(sig, sig2); + + assert!( + VerifyingKey::::verify(&pk, b"a different message", None, &sig) + .is_err() + ); +} + +#[test] +fn pss_shake256_sign_with_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA4096PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +#[test] +fn rsa_pss_4096_shake256_wycheproof_vectors() { + let doc: Value = serde_json::from_str(&get_test_data("rsa_pss_4096_shake256_test.json")) + .expect("valid JSON"); + + let mut num_tests = 0usize; + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], "SHAKE256"); + assert_eq!(group["mgf"], "SHAKE256"); + assert_eq!(group["sLen"], 64); + let n: [u64; 64] = limbs_from_hex(group["publicKey"]["modulus"].as_str().unwrap()); + let e = u32::from_str_radix(group["publicKey"]["publicExponent"].as_str().unwrap(), 16) + .expect("publicExponent fits in u32 for every group here"); + let pk = RSA4096PublicKey::new(&n, e).expect("group public key must be valid"); + + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + + let Ok(sig): Result<[u8; 512], _> = sig_bytes.try_into() else { + assert_ne!(test["result"], "valid", "tcId {tc_id}: wrong-length 'valid' signature"); + num_invalid += 1; + continue; + }; + + let verified = + VerifyingKey::::verify(&pk, &msg, None, &sig).is_ok(); + match test["result"].as_str().unwrap() { + "valid" => { + assert!(verified, "tcId {tc_id}: expected valid, got invalid"); + num_valid += 1; + } + "invalid" => { + assert!(!verified, "tcId {tc_id}: expected invalid, got valid"); + num_invalid += 1; + } + other => panic!("tcId {tc_id}: unknown result {other:?}"), + } + } + } + + assert_eq!(num_tests, 184); + assert_eq!(num_valid, 138); + assert_eq!(num_invalid, 46); +} + +// ---- bouncycastle_core trait conformance ------------------------------------------------------ + +/// The SHAKE-native generic type has had `core-test-framework`'s full suite at RSA-2048/3072 +/// (`RSASSA_PSS_SHAKE128`); this checks the RSA-4096/SHAKE256 alias's width constants by round +/// trips through `SigningKey::sign`. +#[test] +fn pss_shake256_trait_round_trips() { + let sk = genuine_key(); + let pk = RSA4096PublicKey::new(sk.n(), 0x10001).unwrap(); + let msg = b"PSS-SHAKE256 at RSA-4096, both APIs"; + let from_trait = + SigningKey::::sign(&sk, &mut DefaultRNG::default(), msg, None) + .unwrap(); + VerifyingKey::::verify(&pk, msg, None, &from_trait).unwrap(); + VerifyingKey::::verify(&pk, msg, None, &from_trait).unwrap(); + let from_free = + SigningKey::::sign(&sk, &mut DefaultRNG::default(), msg, None) + .unwrap(); + VerifyingKey::::verify(&pk, msg, None, &from_free).unwrap(); + assert!( + VerifyingKey::::verify(&pk, b"other", None, &from_free).is_err() + ); +} diff --git a/crypto/rsa/tests/rsa_4096_tests.rs b/crypto/rsa/tests/rsa_4096_tests.rs new file mode 100644 index 000000000..b3e0eea96 --- /dev/null +++ b/crypto/rsa/tests/rsa_4096_tests.rs @@ -0,0 +1,515 @@ +//! RSA-4096 against Wycheproof: `rsa_pkcs1_4096_sig_gen_test.json` (sign), `rsa_signature_4096_ +//! sha{256,384,512}_test.json` (verify, all three hashes), and `rsa_pss_4096_sha{256,384,512} +//! _mgf1_*_test.json` (PSS verify, all three hashes -- unlike every other size in this crate, +//! Wycheproof has matching-hash-and-MGF PSS vectors for all three at 4096 bits). Only SHA-256 has +//! sig-gen key material to check signing against a known-correct output, so every other sign +//! pairing (PKCS#1 v1.5/SHA-384 and /SHA-512 signing, and PSS signing under all three hashes +//! with both an RNG-drawn and a fixed salt) is exercised by a self-consistency round trip instead, following +//! `rsa_3072_tests.rs`'s own precedent. +//! +//! `p`/`q`/`dP`/`dQ`/`qInv` were recovered from `rsa_pkcs1_4096_sig_gen_test.json`'s SHA-256 +//! group's `(n, e, d)` via the same factoring-from-d method as RSA-2048's key, cross-checked +//! against all 8 of that group's real signatures before being pasted here. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{ + DeterministicSigningKey, SignatureAlgorithm, SigningKey, VerifyingKey, +}; +use bouncycastle_core::traits::SignatureSigner; +use bouncycastle_core_test_framework::signature::{ + TestFrameworkSignature, TestFrameworkSignatureKeys, +}; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::rsa_4096::{ + PK_LEN, RSA4096PrivateKey, RSA4096PublicKey, RSASSA_PKCS1_v1_5_SHA256, + RSASSA_PKCS1_v1_5_SHA384, RSASSA_PKCS1_v1_5_SHA512, RSASSA_PSS_SHA256, RSASSA_PSS_SHA384, + RSASSA_PSS_SHA512, SIG_LEN, SK_LEN, +}; +use serde_json::Value; +use std::fs; +use std::path::Path; + +/// Signs through the streaming trait path with a fixed salt (`set_salt`) -- the +/// deterministic PSS mode, for tests against a known salt. Returns `do_sign_final`'s `Result`. +macro_rules! sign_with_salt { + ($ty:ty, $sk:expr, $msg:expr, $salt:expr) => {{ + let mut signer = + SigningKey::<$ty, 512>::create_signer($sk, &mut DefaultRNG::default(), None).unwrap(); + signer.set_salt($salt); + signer.do_update($msg); + signer.do_final() + }}; +} + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +fn limbs_from_hex(hex: &str) -> [u64; L] { + let bytes_len = 8 * L; + let mut bytes = hex_decode(hex).expect("valid hex"); + if bytes.len() == bytes_len + 1 && bytes[0] == 0 { + bytes.remove(0); + } + assert_eq!(bytes.len(), bytes_len, "expected a {}-bit value", bytes_len * 8); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = bytes_len - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + limbs +} + +fn genuine_key() -> RSA4096PrivateKey { + let p: [u64; 32] = [ + 0x5141acd4afd4771f, 0x5857742b7e4032dc, 0xb7a9b97fe5371396, 0x4b1783c5bc91a6dc, + 0xaf5d0c912d97b728, 0xeb265a7f28b88976, 0x5e993302aa72f11d, 0xc007ad09d76ae22a, + 0x44cbb87b8bec8fd3, 0xf152f15c73b95ac5, 0x8dbdad655fbbad20, 0x59855d0de1c794ac, + 0x4c6060926d878a20, 0xf38860474e44993d, 0x177a91caeef98b13, 0xd1676123544d3def, + 0xc1673063d8487390, 0xfedd55ba2e233a6d, 0x016a5735b883c639, 0xbd813f9fb752805b, + 0x923a7b775b5ffaff, 0xb174e55ca6a12c21, 0x0373077895a6934a, 0x6b725388f54ffea3, + 0x953c09104e1fd2d3, 0x9114f39017871528, 0x33918eb72b786b3c, 0xf43b8626af13d14a, + 0x886a0904eb486140, 0x0037409b81f3a5b3, 0x19e44e26140d2ede, 0xc3c677495c2bd566, + ]; + let q: [u64; 32] = [ + 0xee0745546a6a1c73, 0x8b1050436d882425, 0xdc2e7872ce6fb2b7, 0xf98671ee4899050b, + 0x796a5dd847400804, 0x9ba1f51132355792, 0xc39066a9fc27985b, 0x81f4bca5d15e0962, + 0x6a46cf6dbb6d2ad9, 0xdf3d085ae7693b7e, 0xf58e3c750b6d978a, 0xb52b9000d3a7aab1, + 0xdbb07e23bce1960c, 0x4e291b5c8ddaa6a7, 0x0544a7a7cb3f0792, 0xc695e324fbb11beb, + 0xbded418507f2f175, 0x49fe5f47737ff436, 0xeb37f85b7554d7ad, 0x724d042aef25cea2, + 0xeadbd11dcd2fec7b, 0xb50812705b379f5d, 0xfc50fa98fb67b746, 0x16f2bdf5964b26c7, + 0xcc1c817507859246, 0x6810c2136ad81b16, 0xd7bc78d637e17e0e, 0x6882825347455782, + 0xcdb615d921f5e705, 0x0a982efbe6658e6b, 0x59e96686f7b8752f, 0xc357cf685f9b8d4e, + ]; + let d_p: [u64; 32] = [ + 0x3cd8e91fee6bcdd3, 0x074fdc015ab1c45f, 0x34135987a2288907, 0xf6961d44dfb7505c, + 0x251be32315a85b75, 0xe8982e8493a2ae78, 0xc268493861002977, 0x140fb5806735814e, + 0x35d426d719b12c41, 0xa5f194dbbf374226, 0xa0a6c5314c1a2375, 0x66899d59c04b60c4, + 0x5158d7867dbfec94, 0xec1dde997c00125e, 0x62e5a19b2a71decf, 0x310291892c37e8a2, + 0x223e06c9521f4a45, 0xe4f08e1d5543ed74, 0xcf4dc8de58b9e9ad, 0x9167ad4f122b3263, + 0xf110dbac286712f6, 0x2db237d0f39cda5f, 0xa54b67367c857476, 0x138b37b7b1ed2219, + 0x950d9f4e82aab9bc, 0x90943e671e928e23, 0x44791809c9fcf19f, 0xc70396da054ba478, + 0x77248f6fe00e31bd, 0xf3731b1415d5f2ee, 0xf5242aa6657760cd, 0xa6bbb5460638d2b2, + ]; + let d_q: [u64; 32] = [ + 0xfa935bd36284ea6b, 0x9c255a57d6b3ac0f, 0x35b350881ec52e99, 0xf48d35dd11598957, + 0x61fee5d3763b8cfb, 0xf86f5d1936a161ae, 0x3a8b852507264ae2, 0xe558f90950a572a2, + 0x205aeb15029c95a6, 0x0fa8cf020e960c3b, 0x6a793c66abfb0963, 0xa9c7d7b391d0cbee, + 0x6310f31421e85c6b, 0xc0de89d2edb32796, 0x1cf37af6eab4ed97, 0x1533eacf819c878e, + 0xc700fc7e2a482abe, 0x4d1c62a63d917682, 0x89141558095c3228, 0x029bb05df0b8f121, + 0xa239e00b24ba9b5b, 0x74c3c55d5051f35f, 0x2f45200123860c7d, 0x4fabf224d27abaf1, + 0x0acb80fbf7ed2d86, 0x87db455a8bee03ce, 0x980bbbd1afa8fac6, 0x9d9ee6515ded4930, + 0x4c62a05dae0f744b, 0x69ff90e2839348c9, 0xda141e6d351e42da, 0x178cd58f72bf5118, + ]; + let q_inv: [u64; 32] = [ + 0x5792505b857d3e8d, 0x8a13922b1240c0d2, 0xb5fcb8bf3df1f936, 0x74483606c2cd7ed8, + 0xd40f33bbfd9be093, 0xb5f40ed6632989f5, 0x7f2cc0cd8c8a0661, 0x87630c6b25dddeb5, + 0x7032a5af6ef7c20f, 0x90331600142e6cb9, 0x67b92645b68c6d8f, 0xb63517c7c39843cf, + 0xd50712bf8fc35882, 0x3c421e2447b1a989, 0x7a251afa7aaa6908, 0x43b4de11f99b2654, + 0x1e02735920ef48a3, 0x218fc24da34ce8f0, 0x280446e49b3b9b15, 0x8d96f07411c882bf, + 0x66e283f5d5fb0e6e, 0x0844efd28d853446, 0x5eb4def8dba57b99, 0x474111f92d19f3ab, + 0x748d7b5d94cacd4d, 0xcf634f3a07cea4b7, 0x8dbcca51f4da4379, 0x80774236a54ec9dc, + 0x6a1ef00ee582d3d1, 0xdecfb14ca1e80c8e, 0xc78af5f6c807cc99, 0x484ad86e79415ea3, + ]; + RSA4096PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("recovered CRT components must be accepted") +} + +#[test] +fn pkcs1_v1_5_sig_gen_4096_sha256() { + let sk = genuine_key(); + let doc: Value = + serde_json::from_str(&get_test_data("rsa_pkcs1_4096_sig_gen_test.json")).unwrap(); + let group = doc["testGroups"] + .as_array() + .unwrap() + .iter() + .find(|g| g["sha"] == "SHA-256") + .expect("the SHA-256/4096 group"); + + let mut num_tests = 0usize; + for test in group["tests"].as_array().unwrap() { + num_tests += 1; + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let expected_sig = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let sig = DeterministicSigningKey::::sign_deterministic( + &sk, &msg, None, + ) + .unwrap_or_else(|e| { + panic!("tcId {tc_id}: signing failed: {e:?}"); + }); + assert_eq!(sig.to_vec(), expected_sig, "tcId {tc_id}: signature mismatch"); + } + assert_eq!(num_tests, 8); +} + +/// Mutation testing found that PKCS#1 v1.5/SHA-384 and /SHA-512 signing were never called by any test at +/// this modulus size (the real Wycheproof verify vectors below only exercise `verify`, and only +/// SHA-256 has a sig-gen file to sign against): a whole-function-body mutant replacing either sign +/// function with a constant still passed the whole suite. +#[test] +fn pkcs1_v1_5_sha384_and_sha512_round_trip() { + let sk = genuine_key(); + let pk = RSA4096PublicKey::new(sk.n(), 0x10001).unwrap(); + let sig384 = DeterministicSigningKey::::sign_deterministic( + &sk, b"hello", None, + ) + .unwrap(); + VerifyingKey::::verify(&pk, b"hello", None, &sig384).unwrap(); + let sig512 = DeterministicSigningKey::::sign_deterministic( + &sk, b"hello", None, + ) + .unwrap(); + VerifyingKey::::verify(&pk, b"hello", None, &sig512).unwrap(); +} + +fn run_pkcs1_v1_5_verify_vectors( + filename: &str, + verify: impl Fn(&RSA4096PublicKey, &[u8], &[u8; 512]) -> bool, + expected_sha: &str, + expected_valid: usize, + expected_invalid: usize, +) { + let doc: Value = serde_json::from_str(&get_test_data(filename)).expect("valid JSON"); + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + let mut num_missing_null = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], expected_sha); + let n: [u64; 64] = limbs_from_hex(group["publicKey"]["modulus"].as_str().unwrap()); + let e = u32::from_str_radix(group["publicKey"]["publicExponent"].as_str().unwrap(), 16) + .unwrap(); + let pk = RSA4096PublicKey::new(&n, e).unwrap(); + + for test in group["tests"].as_array().unwrap() { + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let flags: Vec<&str> = + test["flags"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect(); + + let Ok(sig): Result<[u8; 512], _> = sig_bytes.try_into() else { + assert_ne!(test["result"], "valid"); + num_invalid += 1; + continue; + }; + let verified = verify(&pk, &msg, &sig); + match test["result"].as_str().unwrap() { + "valid" => { + assert!(verified, "tcId {tc_id}: expected valid, got invalid"); + num_valid += 1; + } + "invalid" => { + assert!(!verified, "tcId {tc_id}: expected invalid, got valid"); + num_invalid += 1; + } + "acceptable" => { + assert_eq!(flags, vec!["MissingNull"], "tcId {tc_id}: unreviewed acceptable"); + assert!(verified); + num_missing_null += 1; + } + other => panic!("tcId {tc_id}: unknown result {other:?}"), + } + } + } + assert_eq!(num_missing_null, 1); + assert_eq!(num_valid, expected_valid, "for {filename}"); + assert_eq!(num_invalid, expected_invalid, "for {filename}"); +} + +#[test] +fn rsa_signature_4096_sha256_wycheproof_vectors() { + run_pkcs1_v1_5_verify_vectors( + "rsa_signature_4096_sha256_test.json", + |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }, + "SHA-256", + 7, + 250, + ); +} + +#[test] +fn rsa_signature_4096_sha384_wycheproof_vectors() { + run_pkcs1_v1_5_verify_vectors( + "rsa_signature_4096_sha384_test.json", + |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }, + "SHA-384", + 7, + 251, + ); +} + +#[test] +fn rsa_signature_4096_sha512_wycheproof_vectors() { + run_pkcs1_v1_5_verify_vectors( + "rsa_signature_4096_sha512_test.json", + |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }, + "SHA-512", + 7, + 251, + ); +} + +fn run_pss_verify_vectors( + filename: &str, + verify: impl Fn(&RSA4096PublicKey, &[u8], &[u8; 512]) -> bool, + expected_sha: &str, + expected_slen: u64, + expected_valid: usize, + expected_invalid: usize, +) { + let doc: Value = serde_json::from_str(&get_test_data(filename)).expect("valid JSON"); + let mut num_valid = 0usize; + let mut num_invalid = 0usize; + + for group in doc["testGroups"].as_array().unwrap() { + assert_eq!(group["sha"], expected_sha); + assert_eq!(group["mgfSha"], expected_sha); + assert_eq!(group["sLen"], expected_slen); + let n: [u64; 64] = limbs_from_hex(group["publicKey"]["modulus"].as_str().unwrap()); + let e = u32::from_str_radix(group["publicKey"]["publicExponent"].as_str().unwrap(), 16) + .unwrap(); + let pk = RSA4096PublicKey::new(&n, e).unwrap(); + + for test in group["tests"].as_array().unwrap() { + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let Ok(sig): Result<[u8; 512], _> = sig_bytes.try_into() else { + assert_ne!(test["result"], "valid"); + num_invalid += 1; + continue; + }; + let verified = verify(&pk, &msg, &sig); + match test["result"].as_str().unwrap() { + "valid" => { + assert!(verified, "tcId {tc_id}: expected valid"); + num_valid += 1; + } + "invalid" => { + assert!(!verified, "tcId {tc_id}: expected invalid"); + num_invalid += 1; + } + other => panic!("unknown result {other:?}"), + } + } + } + assert_eq!(num_valid, expected_valid, "for {filename}"); + assert_eq!(num_invalid, expected_invalid, "for {filename}"); +} + +#[test] +fn rsa_pss_4096_sha256_mgf1_32_wycheproof_vectors() { + run_pss_verify_vectors( + "rsa_pss_4096_sha256_mgf1_32_test.json", + |pk, msg, sig| VerifyingKey::::verify(pk, msg, None, sig).is_ok(), + "SHA-256", + 32, + 63, + 45, + ); +} + +#[test] +fn rsa_pss_4096_sha384_mgf1_48_wycheproof_vectors() { + run_pss_verify_vectors( + "rsa_pss_4096_sha384_mgf1_48_test.json", + |pk, msg, sig| VerifyingKey::::verify(pk, msg, None, sig).is_ok(), + "SHA-384", + 48, + 95, + 46, + ); +} + +#[test] +fn rsa_pss_4096_sha512_mgf1_64_wycheproof_vectors() { + run_pss_verify_vectors( + "rsa_pss_4096_sha512_mgf1_64_test.json", + |pk, msg, sig| VerifyingKey::::verify(pk, msg, None, sig).is_ok(), + "SHA-512", + 64, + 132, + 47, + ); +} + +/// Mutation testing found that none of the six PSS signing paths (three hashes, RNG-drawn or fixed salt) +/// were ever called at this modulus size: the real Wycheproof PSS vectors above only exercise +/// `verify` (against externally-sourced signatures), so a whole-function-body mutant replacing any +/// sign function with a constant still passed the whole suite. One fixed-salt-round-trip-plus- +/// rejection test and one RNG-freshness test per hash, matching +/// `rsa_2048_pss_sha384_sha512_tests.rs`'s shape, calls each function at least once. +#[test] +fn pss_sha256_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA4096PublicKey::new(sk.n(), 0x10001).unwrap(); + let salt = [0x22u8; 32]; + let sig = + sign_with_salt!(RSASSA_PSS_SHA256, &sk, b"hello", salt).expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig).expect("must verify"); + assert!(VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err()); +} + +#[test] +fn pss_sha256_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA4096PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +#[test] +fn pss_sha384_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA4096PublicKey::new(sk.n(), 0x10001).unwrap(); + let salt = [0x11u8; 48]; + let sig = + sign_with_salt!(RSASSA_PSS_SHA384, &sk, b"hello", salt).expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig).expect("must verify"); + assert!(VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err()); +} + +#[test] +fn pss_sha384_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA4096PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +#[test] +fn pss_sha512_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA4096PublicKey::new(sk.n(), 0x10001).unwrap(); + let salt = [0x33u8; 64]; + let sig = + sign_with_salt!(RSASSA_PSS_SHA512, &sk, b"hello", salt).expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig).expect("must verify"); + assert!(VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err()); +} + +#[test] +fn pss_sha512_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA4096PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +// ---- bouncycastle_core trait conformance ------------------------------------------------------ + +fn fixed_keypair() -> Result<(RSA4096PublicKey, RSA4096PrivateKey), SignatureError> { + let sk = genuine_key(); + let pk = RSA4096PublicKey::new(sk.n(), 0x10001)?; + Ok((pk, sk)) +} + +/// `core-test-framework`'s full conformance suite for one deterministic and one randomized +/// pairing at this width. An RSA-4096 private-key operation is slow enough in a debug build that +/// the suite's dozen signatures per pairing add up, so the remaining pairings get +/// [`trait_round_trip`] instead: the generic code they share has already had the full suite at +/// RSA-2048 and RSA-3072, and only this size's width constants are new. +#[test] +fn sha256_trait_conformance_suites() { + TestFrameworkSignature::new(false).test_deterministic_signature::(fixed_keypair, false); + TestFrameworkSignature::new(false) + .test_signature::( + fixed_keypair, false, + ); +} + +#[test] +fn key_trait_boundary_conditions() { + TestFrameworkSignatureKeys::new() + .test_keys::(fixed_keypair); +} + +/// Sign one-shot and streamed, verify both, and reject a different message -- what a wrong width +/// constant in this size's aliases would break. PSS, through `SigningKey`. +fn pss_round_trip(pk: &RSA4096PublicKey, sk: &RSA4096PrivateKey) +where + RSA4096PrivateKey: SigningKey, + RSA4096PublicKey: VerifyingKey, +{ + let msg = b"RSA-4096 trait round trip"; + let mut rng = DefaultRNG::default(); + let sig = SigningKey::::sign(sk, &mut rng, msg, None).unwrap(); + VerifyingKey::::verify(pk, msg, None, &sig).unwrap(); + let mut signer = SigningKey::::create_signer(sk, &mut rng, None).unwrap(); + signer.do_update(&msg[..8]); + signer.do_update(&msg[8..]); + VerifyingKey::::verify(pk, msg, None, &signer.do_final().unwrap()).unwrap(); + assert!(VerifyingKey::::verify(pk, b"a different message", None, &sig).is_err()); +} + +/// As [`pss_round_trip`], for PKCS#1 v1.5 through `DeterministicSigningKey`. +fn pkcs1_round_trip(pk: &RSA4096PublicKey, sk: &RSA4096PrivateKey) +where + RSA4096PrivateKey: DeterministicSigningKey, + RSA4096PublicKey: VerifyingKey, +{ + let msg = b"RSA-4096 trait round trip"; + let sig = DeterministicSigningKey::::sign_deterministic(sk, msg, None).unwrap(); + VerifyingKey::::verify(pk, msg, None, &sig).unwrap(); + let mut signer = + DeterministicSigningKey::::create_signer_deterministic(sk, None).unwrap(); + signer.do_update(&msg[..8]); + signer.do_update(&msg[8..]); + assert_eq!(signer.do_final().unwrap(), sig); + assert!(VerifyingKey::::verify(pk, b"a different message", None, &sig).is_err()); +} + +#[test] +fn remaining_pairings_trait_round_trips() { + let (pk, sk) = fixed_keypair().unwrap(); + pkcs1_round_trip::(&pk, &sk); + pkcs1_round_trip::(&pk, &sk); + pss_round_trip::(&pk, &sk); + pss_round_trip::(&pk, &sk); +} diff --git a/crypto/rsa/tests/rsa_8192_tests.rs b/crypto/rsa/tests/rsa_8192_tests.rs new file mode 100644 index 000000000..4445cb5e9 --- /dev/null +++ b/crypto/rsa/tests/rsa_8192_tests.rs @@ -0,0 +1,495 @@ +//! RSA-8192. Wycheproof has no `rsa_pkcs1_8192_sig_gen_test.json` and no matching-hash-and-MGF +//! PSS vector file at this size, so there is no real key to recover the way every smaller size in +//! this crate uses. Two independent checks stand in for that: +//! +//! 1. **BC Java cross-check.** A fresh 8192-bit keypair (two independently generated, +//! Miller-Rabin-tested 4096-bit primes, `e = 65537`) was fed to BC Java's own RSA +//! (`java.security.Signature` via the `BC` provider, `SHA{256,384,512}withRSA` and +//! `SHA{256,384,512}withRSAandMGF1`) as an `RSAPrivateCrtKeySpec`, signing the UTF-8 bytes of +//! `"hello"`. PKCS#1 v1.5 is deterministic, so its three signatures are checked here byte for +//! byte against this crate's own PKCS#1 v1.5 signing (SHA-256/384/512); PSS is randomized, so its +//! three signatures (BC Java's default PSS: salt length equal to the digest length, matching +//! this crate's own convention) are instead checked with this crate's own +//! PSS verifier, a genuine independent-implementation cross-check even though +//! it cannot be a byte-for-byte one. +//! 2. **Real Wycheproof verify vectors, sampled.** `rsa_signature_8192_sha{256,384,512}_test.json` +//! exist (verify-only, with their own unrelated key) and are used here, but not exhaustively: +//! each has on the order of 260 vectors, and at `L = 128` a single verify costs roughly 19x +//! what one costs at RSA-3072 (`(128/48)^3`), which would make a full run of three such files +//! alone take on the order of an hour. Every *distinct* `(result, flags)` combination present +//! in each file is still exercised at least once (deduplicated below), so every failure mode +//! Wycheproof encodes for this hash is checked -- just not every repetition of it. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{ + DeterministicSigningKey, SignatureAlgorithm, SigningKey, VerifyingKey, +}; +use bouncycastle_core::traits::SignatureSigner; +use bouncycastle_core_test_framework::signature::{ + TestFrameworkSignature, TestFrameworkSignatureKeys, +}; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rsa::rsa_8192::{ + PK_LEN, RSA8192PrivateKey, RSA8192PublicKey, RSASSA_PKCS1_v1_5_SHA256, + RSASSA_PKCS1_v1_5_SHA384, RSASSA_PKCS1_v1_5_SHA512, RSASSA_PSS_SHA256, RSASSA_PSS_SHA384, + RSASSA_PSS_SHA512, SIG_LEN, SK_LEN, +}; +use serde_json::Value; +use std::collections::HashSet; +use std::fs; +use std::path::Path; + +/// Signs through the streaming trait path with a fixed salt (`set_salt`) -- the +/// deterministic PSS mode, for tests against a known salt. Returns `do_sign_final`'s `Result`. +macro_rules! sign_with_salt { + ($ty:ty, $sk:expr, $msg:expr, $salt:expr) => {{ + let mut signer = + SigningKey::<$ty, 1024>::create_signer($sk, &mut DefaultRNG::default(), None).unwrap(); + signer.set_salt($salt); + signer.do_update($msg); + signer.do_final() + }}; +} + +const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; +const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + +fn get_test_data(filename: &str) -> String { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return fs::read_to_string(path).unwrap(); + } + } + panic!( + "wycheproof not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +fn limbs_from_hex(hex: &str) -> [u64; L] { + let bytes_len = 8 * L; + let mut bytes = hex_decode(hex).expect("valid hex"); + if bytes.len() == bytes_len + 1 && bytes[0] == 0 { + bytes.remove(0); + } + assert_eq!(bytes.len(), bytes_len, "expected a {}-bit value", bytes_len * 8); + let mut limbs = [0u64; L]; + for i in 0..L { + let start = bytes_len - (i + 1) * 8; + limbs[i] = u64::from_be_bytes(bytes[start..start + 8].try_into().unwrap()); + } + limbs +} + +/// The fresh, self-generated key used for the BC Java cross-check and for self-consistency. +fn genuine_key() -> RSA8192PrivateKey { + let p: [u64; 64] = [ + 0xa9a87892c469b1e5, 0x038c3f4efcb198ac, 0xed4f14df1b8a1e1f, 0xef2330d78ee73020, + 0xd3093ddaf276904c, 0x94a438d69aeaff23, 0x48dbb3d700ad58ed, 0xbbd763b5346ff916, + 0xea402fb475938e3e, 0xcaed4c45c543aff3, 0xc0954c4255b90f57, 0x2fe6242da64a5099, + 0x3fd54a6b6175342c, 0xdf8dd0187588abbd, 0x4abc11d33376e220, 0x90d3482c7fc1397d, + 0xde925fb5a2cb1e7e, 0x062e99dfbd3c790b, 0x243ef703c0912a1e, 0x91ee31a02ef1a02e, + 0x13b19d3960842877, 0xc6e8036e26c4a339, 0xf2606f45a73314c9, 0x5dfe4c6dd0aa04ee, + 0x1385bfde55204d94, 0x454123829d28200d, 0x18a3d86f516f2a50, 0xcbe5e841cd2fdb17, + 0xc86825aae3031a2c, 0xf4726a6681852b6e, 0x3f16b45a496eee29, 0xd5c4b2319abff492, + 0x71077736745aee55, 0x72586dba7b765bbe, 0x681813f6851efdce, 0x196c13fa6d6781d2, + 0x4712ba3eb0734e43, 0xde2a9a8da6da8340, 0xc2f93a2af8ffbaaa, 0x4db552ab82a577ae, + 0x07c075eb13064f67, 0x818a7d0b31680682, 0xa58320f732d69460, 0xa5922479f4aaf594, + 0x4d545baa4cd73f39, 0xa20c87a2660869c4, 0x8350211511fe7d7a, 0x50174a372234ba89, + 0x6eefd36b20bd1ce7, 0x07d124cb61d25c8b, 0x7356e6380041dcd5, 0x6a7c98caab76211a, + 0x61ff7eaac65f815d, 0x1167dc0b28f418b3, 0x9fc8bf056efe8677, 0xf5ce4481e1670e10, + 0x6ae488da222292de, 0xcc786e194ea84d2a, 0x41b134cb6a8ae4f5, 0x870ff94b064466b7, + 0xc82ad608c14a9385, 0x5338550ac9d3f96d, 0xdce87e0d41f1ba9d, 0xef9beedb39920589, + ]; + let q: [u64; 64] = [ + 0x08798e2dd82b7d9f, 0x30208e668897171d, 0x8f73c28d70144ec6, 0x66f64ce7d2402ae0, + 0xdbc26c601d5cc3a3, 0x657176891201beee, 0x692812d7b9b796c5, 0xae27fa9062406a23, + 0x7b4e6ee5b4d2d22f, 0xd846e0e8fbeb0108, 0x52ca75d6866b780b, 0x2620152d7bb490c0, + 0xdca773f826ba9118, 0xb308499a61d0a4eb, 0x44ba0f58bff07ed5, 0x268483fe5829eb5c, + 0x02e7f0c1d3ee3856, 0x76eae3a004d0f37a, 0x99d9cb3aab5b3f1f, 0x0c9f76678a44f2c4, + 0xc82ba3f75883e512, 0x57719c85e8888746, 0x63c967ca1b81cf17, 0x6f4942a86e96bb6a, + 0x614105324fdb77ae, 0xdb6a0c777f7a4bbc, 0x184bf293127472d5, 0xc26798126a967a8f, + 0x45fa10ccadd5ef6e, 0x13cacbbf5cbc2722, 0x23b4e2f2d18785fd, 0x83a4d39485ad254b, + 0x3a096f5ddd674174, 0x6df8f2ed81ae9e7e, 0x77ee2d29fb78201d, 0x4b4c10c9ac8853eb, + 0x35690c51d0ff6208, 0x8de9a271cb50aa59, 0x51b0701f8cdd1505, 0x4cb715c9b578c63b, + 0x3c9b35776d94c3c7, 0xe952d6a7c34a2abb, 0xc432f1fcc2a5105f, 0x4f7f23a62cea2f81, + 0xfa507889f33eedd9, 0x7e37e435cb776085, 0x576b2ad8a4f39820, 0x0a85d1466d55ad69, + 0xb1dee047621be714, 0x216c6b3c1c945527, 0xe6298f6e1575e39e, 0xdd16506432adc399, + 0xc1c065319c823d2e, 0xf2d6f25d6f34a582, 0x75d6801f595c50fc, 0xccfcd556c20e1130, + 0xa24e6a1396173d85, 0xe2cbc5218657c80a, 0xe7bec5bf5a944815, 0x656f5fa54bf06f6c, + 0x7b68b002b0ea62d7, 0xa2eb84753d291781, 0xdd329e1ce00c2087, 0xcdb932d13e38ee0a, + ]; + let d_p: [u64; 64] = [ + 0x848c4d06bcab4c19, 0x2fa349116ae2bfd6, 0xcf9ea12f426f7eb3, 0xfa71d15136d85dff, + 0xc3e3a026e3968f1f, 0xd50f8056de82a33e, 0x68213465c1b3b2d7, 0x426387f62ee9c62e, + 0xeb14efe18920a873, 0xf3c385a60c9dc86c, 0x294ffd8852c4d95e, 0x940d14b53927fef5, + 0x3f709e3b4c8390c3, 0x3b9737d39c42994b, 0x5c02182c4dac78fe, 0x3b6b7c01fe0a0737, + 0x7adeedf6194f6d2c, 0x43c30f449d8ae36c, 0xb88cebf30dac95d4, 0x09fc3d623b8ceb6b, + 0x5d28293b759f2b09, 0xd350dee095846993, 0x1b323dc92b794164, 0xe19d52f0e7ef34bb, + 0x5ff017da15232c02, 0x58fa8d3b24b82973, 0x1c8aed916227e53f, 0xad49cd8938c909d4, + 0xc3d98be0501534a1, 0x91cec83019bd5bb6, 0x985b814d838ffdd2, 0x11df5cb357c86888, + 0xbc0d5590336b8dcf, 0xc25fa85b1912ac66, 0x390c8752384cadef, 0x07b54e62df65825a, + 0x7f7ad57e592cfc3b, 0xed1b7f53d6fe2a09, 0x41d9e577118adb13, 0xb6706780ace45139, + 0xbfbaadef8d19443b, 0x3dc42a1dccca1d87, 0xdb8a72a8ca8d4de7, 0xe10dec18507dbc43, + 0x66343911cfeb0313, 0xd29def907cf51b72, 0x998add714f12ef6f, 0x1c394a4b8053f8ec, + 0x46e0b76d81a07852, 0x498c3f81cb9f2b8d, 0x8fd0019a024e19be, 0xf2d405423b1d94e4, + 0x29ce52d9c49ff544, 0xd72643a03a2f9053, 0x1be76a355cfa02d2, 0x8db921cec169edf6, + 0x728f91a0f7d9dc15, 0xe34e3a4f3d41d26d, 0xab0a691f05332eb8, 0xb6241cc75fd5ee1b, + 0x5b8582afd5c1acda, 0xe405bceba411da3e, 0xd849aacac396ef0e, 0x57b5a6ebccd92cf9, + ]; + let d_q: [u64; 64] = [ + 0x77a312e8aa49ed35, 0x3bdc007b0396dd44, 0x187af52b1d2d6e88, 0x7d84fae3e1b134fa, + 0xb1af50faa281bf7d, 0xf868401279f57864, 0x1dba1e456f88eb01, 0xe2ee28d0f43ba792, + 0x3450781c72a68626, 0x30757ab4889c0c41, 0x7f34c53326389381, 0xa28e969fda4bc5e4, + 0x0ed664e6b7a53062, 0xfc9ad2c9c2bd3cf2, 0x1a7bb5627d58e8ba, 0x68b8c4da5a5a5104, + 0x5a0e586f0ebe7967, 0x2dcff7ca08c9b702, 0x89a724facd778478, 0x027fe07b2ad90a40, + 0xd2f67b0b487433d5, 0x28d451f2f0eaa3ad, 0xee8d5d55700c24ca, 0x584202d4e805a701, + 0xa1c40f0ed77d96a5, 0xc4055e480f1674ba, 0xf9fb2aeb561bf104, 0x6b846fe9ef936161, + 0x128295b5f01cf2ad, 0x4aaff225e483b5dd, 0x7cba43b61174a21c, 0xf68f9c7b4474bcbd, + 0xc8979d3dd627346e, 0x9c2b543e42af83ed, 0x221d3779bb596a50, 0xac4d7c5545e7c88b, + 0xa44c83a4cb2ffe15, 0x414d71c3a67b3563, 0xe22dd5c34f66bf5e, 0x6a9cd6374f4513e7, + 0x27765c4e3c8ee4d5, 0xf632603a9e123d3f, 0x748b0b2d39fcb6bd, 0x0bde2fc8edf891e6, + 0xded2b9cf10705222, 0xe5ca5d358eb8067a, 0x23368f3218b1bd0c, 0xe41d6519183900d0, + 0x36d90382237f0987, 0x858624f4663a543a, 0x2309265e924e5f4c, 0xfc548ae0263785e8, + 0xe010d5d5e76f3406, 0x963cc28c4228ff36, 0x4245a573c17ac537, 0x7e9ab38877e4d606, + 0xc69b8cfcee4fba17, 0xe127881203d02766, 0xf3b33254ff10a7b7, 0x4df7b087512a53dd, + 0x28ffecc1c7a3ee5d, 0x4c693f99aa4b22b7, 0x498cdaef4c3d1223, 0x78c24ba834042b07, + ]; + let q_inv: [u64; 64] = [ + 0xd87df1de9c759f36, 0x3ee1b8b22c8eab2d, 0xd6098595c2696717, 0xa922e4722b8304de, + 0x424bec1c55c1db54, 0x918f2f8bf0ac261a, 0x1e190b2089185564, 0xc0dedbfc6ee606bb, + 0x1d1d701caca565c9, 0x2d735f7a1c5b9d67, 0x8339684f656205db, 0xc2dc06896727d9ce, + 0x6c8d6fe334141d31, 0x5245e157c87cd8b4, 0x4975b68eb2eaad9a, 0xfbc3389ffb0e788d, + 0x5e9a45250d135847, 0x676e7e6a128cfa88, 0x74341d8b28dcbcb8, 0x8f7d07f37a0490e0, + 0x83a0eae9ecc99a0e, 0xa8188892180dc052, 0x3cd79e1eea7809cb, 0x3bcc1d769d917b24, + 0x1d8dc8edc7268e8d, 0x3ce2707055f522ca, 0x163ead0ca2eb5fee, 0xf147fb695aa70022, + 0xab406a6c89161f7d, 0x8fbbfe117a95f46e, 0xa16d4e65396165bd, 0xb54f52ab1bb357e5, + 0x67cdec7b6a349ed9, 0x303875e0d97cf3fa, 0x0af9378ecb114fd8, 0x4ada6f45b9d6b1df, + 0x9ef2cf55de53d0ed, 0x33e4c2070b165b52, 0x3feddf0f7a1158c0, 0x91463a2b1952e83a, + 0x65a9db73de55ef6b, 0x8e18c716a69a9a6a, 0x0ca44356ca6e2898, 0x7df214b19f2114f1, + 0x9af2aee9d5cf1077, 0x22fc8b019fd3d0ff, 0x50c6eead29ab18cd, 0xa7e6621f8eae45fe, + 0x588bde2d99b8692d, 0x339be0a07b4f0a7b, 0xcbdbdcc8717105d2, 0x78bbd3e3392a503e, + 0xb868f9db1cf14f74, 0x52f3dc7f028c9f7e, 0xa3629e034b453834, 0x52bab134f3ae91a2, + 0xd1aac2fe108f98c0, 0x1e799537a42b3060, 0xdbcce6fc287e449b, 0x1985969e5afbc524, + 0x61ba61f820f83597, 0x65c8523f0767bccc, 0xde04e1644a214f9a, 0x9748f8d853be7131, + ]; + RSA8192PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv) + .expect("self-generated CRT components must be accepted") +} + +const BC_JAVA_SHA256_SIG: &str = "be0dceec05a9aeff34565a9c7e9d2a63f9407d1f17e9786d45b49ee04d872155710b0d2aa4974398fa9b31ab0c61d0fe207fea57d7878736fa05ca44d22cb7d101b8e15d7a1d60c44bd394048d1ba988f351e3c780857a761d6f8a78bbaa150d5e0f3dd920b9c55a6b80541d26e50b343c0c1e3e4bfade12c8d0b7827b49663d8b2ea9196793c1b46bb4128cee5012f8b045d09ab96a172b35c341eaeed6447e02d02aa47c47ba9863052202325bf0e1378b6174d69d46a97d0419861e773272e852f08231a64badc22448d1ea036376de4c2627454803b690613e2c77a0a440bf55208b55a401c1a186c48eebdf0ec386d99dbf1602417c4b6185b4323aaf4805063511e45f2b0e80d7501b54339a491739a3e8c84690648e870c2811d40e20d6eb91d0f493e41e68e5f5dacc617c152e4c8acd5693c25860bdd03f0b64ad409397f196d781e9ffb0c10499662465ca29448359d343290433187ad46d50e864bcfe60b9fa7b67bd0c269baeeed2e23bed574a41600afe8947dda16947eb505d105d11512bdfff1b4853fb65602c9c8cfcbf3a50be13ca4af99c05ba3d9598e2b1f1a174594bc9fc0828fc29ca33343a4149d6bab10fa6a5063af111871777c86fad6952e590f8fc34c7c86637df1d40914109ecdb99a17ae708089eb976228af03783fc2379ef24b45d1fc3e8b875c196cf20aead62cfa968d97899828ca8d92e62d069cc0db0139250b842379d28c0bbfb7e79c9af376b265d14b7383b4c06a4c9fa953b3b413e52c4a87e61afe9bb79dc7ae5ef0d1bbed84b33fb49e105d3a9e9b5c11c83cb6299895c000a5bb68186bb2f29c78bce0ec494edcd79557a7f94faa24c009cee9a429a5e150b9204146056b19b3cfa2527dad32a263b558d51cf0aa12e12f0b8054e83b11caf9c7a49b10cefb5e63f08612b552b335db1ff182c818beeb618f5b6c108cb35e23cea812da2ad26f0af20e4f48b1586d65a563a240d52922ead451526f982a8b6aa3855836c5f274c979fbfca225f2ba4e68b4f233e3995df5471b5716a0b979a524535fed2f6ea67824b2f37d98534f78033bc2858a5aa5179761ba5e9d8458045427cbd035e111dbc35f67c0848890236bb33c1e4bf64881cb62747c50634a008cae2bffd1fd840c2fe4aa3d428e58b741034f89b5056ebd452061b2d3019c19df65122644ead8f0b6085004e4df187f515f11b003e971898845073472878186cd307b6ae1a30c0ad7994b63591bf702d27c191386f4616b99159527de7fd6f27f1200d310011ed3bb5f8c6ec4fb73d5f2c42485cedfacab9051a44c3b81d3631376ffcc5034d33f83fed3e94421cc6413b3be7d662f9400cf7c1d61529ab4dce3db9993d38f654db3edcd143929dd5ec0ed70ba68d834f7fed79b94bb519278440f395ffe623b5b321eb81fac2f6d9ae47a1"; +const BC_JAVA_SHA384_SIG: &str = "bac97ad2bfcde4bc9793c15cad36a4bcab91534800ed5d2d2296bc8287dfbd1e529586eba90cc83bf228be988ef5533c56c8730770f4da0367ffe9eafbbe580c21a968902eab47a84498babe8182d1d36080f856f3c1a0af9bbefe4f90fb430ff82639df5e8278d28a77eab919b0b866d04942d19743c0f94a3feadc03b1c3c7b70aca244528b542e80f6e4bc04e53f6a3489c6c4d5e04e0db3d84c28c96121729b6447de494fc2f00c5441a4a6559dcd611b7f7565124e07122a9b09ffc43f60bd2ba278e780ba906050757dfd9b97acbb0d79654e9f3f2e9d0c9d87cb3767c7a83c6d74f9bd4e3066052fc35a57c074432b80fb0808328f1a6e38b1b5cc3a2826d67600b442852d26e043f5840492541aa574688393546d68d242000964e7cbbf8819b4ee5027c71f1fcc9c31667a3ff9c46f7d2d600bb437b4ea03fb2aabfe5430217021a72b26c27cd0b5933bb189f13cc6fbbab295ead06d9895c6b63063e0088549c5f1df6a6cd1bc59721ccf198034575875bbccb7c4784c6e6bf23d6a10fd2b929be9bf050939adc82ee71ad76b273e38bb2dddb262ba89d9e04257bce8c0b46329b003b5b06c4ea2bf913692caf787bcece575b8bf8744cb2add09199e9207aca80037582973412e639f4616bd588112ed65ebcec01e591edbba7985697248a0396486697e5407e29eca97b7706ea1ec66d46456013cdf8b6b761a44f62ff7a5e5537a32b8bdff134dbb9b732c5267a4a1da698997bceb25e23226efe8aca0f36c8758e1efe58c6e68dbb361732e4d2de70c8053ea9d979355bcbaeccfc44affc4406685906b377962756d5c6390993e73e2c2916400f598c5e6716ff9fe5551318096f1f1d696e00176e213a4e5ea82eadea4923e87d897e8f7829b45b94afc6bec7da169abafea907caaf5a511a99ce75ab31b8cd6c10c2fa9fc84b4fe67842c33521730fc7e18ee7fddd3e939bd5745caf28e5d56ac132da98b18772280b6d331d9702754a226dac161f8496a06c564d9a5e0a0b3496f45d7786d2f21c564a22c765cd2c79765e15fdff972873302c74ad6a346a71becc8de3e21e1a1be7198e81a05c9daeab8cb11e20099cc044c807f40222caf7125d6d0f79b4f9a6210183da8c2655e7f40bd7932024924c1bf253aad02e0d9c8ba95500fea3da66b9d0beedda9c7bb9f77da44237251d30d4d756b672c0d5f0cdb3878ead02768249276b3ef893005894299b505ba44c9a42d0c418b4907f28a374d8641cedbf65ce0022b294aafc8f8365b3c53ef2b0681be5b4ee168929d25289c55bfc8a4f753add94e95dc00d07e72b7e26fda36ac4ca61134b039dba30dbbd1f4eb30b8950be69fc1f1adfa70774f27d804c156962ac8173b9705a7474dc26bc34a37b2013b667848a66bc2d9d965f74f84b36694897b057fee496ffa2e2d59b19b4"; +const BC_JAVA_SHA512_SIG: &str = "3e06407d4cdc681b2bb37ba5a6d73358bee4a19629d29e7ecfe017d907ed1109b7c1941d789fea049ddf4fc4f4e8113a0a3c239dd7eb808cb4ec50bc5f0c30b88ece92fd2de2dc95e983bafc3c64abc513856db49d4b7d953df014759f8eaf70bdc94b30bbe7c63d737d2b8a34a0e6446941204c1513e4c73aa186b95759535bcf394730022b8b5a01b60557f216ecb5a1ecde7966365e469bd64099475fee4df3de87e29be5cc00f243c7d6b88b1f1df176b200939cf760d7aac7f408dc9b7b2c12f3b1ff6c5e59dc0983bc471503c00a718bd06738cbc05e8940225943dd34ebae3f0431fbf0d50b032f2712d479f2bdc7dca856f040709c419f064c592af4a0faecb3780546554070f8e51f5e52c8686d9046c38f134a67192eb2f95c3bffc1fe0c3186db55e89bef8ce5bfb087a1e7f825127002056bc59181827292c015fa7ed0e858b3ec915eff8d945c254814218c73a61bd0d0060ea7974f64284cf94dfdbee1d98fc93bf1752e2710abbbf83ff9f63f80955902e2def49ee519e0103f0d38ce5d68b122a82bd88d1c4ff6816380048aa6e47f72e1cfacb8e8ec53499b38b8822a2bdc559fcf877a121963af5ea46f75f5e11b51253161c32794aafb3fcbdc9a8670801ef9220c9603ce99cdb6354d3f887f848cbfac484e39424fbb4a758007e49fe62b2f662c208df5151ee7b45bf5b33031f9247267d71a68634daadf0f5174727a8a6885452a226bf0d9143e47242bc61d1c16c475ce797bbef32773467d070f7bef2a3dbdd93d320162c587d1a3e302d5ee74109bf9e185d9a0934e256bb6c92fd5cfd98f0241370f3fc81d349d77501de922d3c3090e720ed26505986e3170f06affbea59193a91a8b85abdd170f38380d7587325ddad1a91b31bc96943535adfad65735d22fd8cadc0347055eb49ea6101adc60afdddb95d33adcbb4caeb50fd63e4a12fbeb856049326c508b2abb26c33275e5c994689053689d3c9bd365d5d4f0776d6316b605bd126d4db55334ac44ad2f0f7ea53ec984a0e82a4b800caade6a74088d0eda31c12e3133138fa86c44c63ad67a63b534ef1ec278c857a07dc7974b459af4dbd21ffd040c691a89480d1f34ab80d668e65a988996f21c9121696b80bc710e33d4835ee7e66136506d4ed75d15ebaa9ddb5f4b64fc8baa145a2978debeb8c5c623f4f05c6b17f392dbd9d82a8081519a45344bf15f6e11addc6e444777c56db09ac8f8d3982e596552f0259dcebc378104031bc4d699b75b450ad7d883e0545914e31ec24c3d503ac157f99528f2746c112c3282803db9672245469b70e7f48d3fc83eb3cc808b790d03ef2e602018308d2855c5e4eb57cdf89d1ea1eaf3673c73401ac8d0193263e84e17eb6e8ba87bc29def2d651f3fc677ea5a8733b20478b3ef5a56e92703a5dfc9a86fceeb677dd034"; +const BC_JAVA_PSS_SHA256_SIG: &str = "71c1b3dd982fd6e1e8be1e20c8a4d42fbe6a8b204af54ab84b63f61b8c39efd20b75ebf477463869a21cff0956f304db041d8cc3357b9ebd92390a33a43e8b89d7c8549fd126f2606df61f97c068aea739bf831276dc8ce002e522c2a46465d649daa1ad92bc7d8d46be6d97839dfdadedd5f2f2471f83ac1facd237f673336e5efbcb7b87f44f80414e1f9479870f5624f3eb2c279ff64e99008e715bfcf7f02b32f1aac3e90cea194ddb7701367db41ad8c59ea635783a9ea724626f5d021a3758161ae1bd50b699a314e0a1a92fac184a9eb97ec44fc1c47000bc6da9267f302949aa837e1bfe45a09d526c974d1c5e7a12deb58817ce0cfde09b87e1cf4948df5c3f13d2d11d5dc926d4fd161b273d5f1195a7a8a174aa3f78f391273fef28b21c2a1f1ffe849c27c7545b199cf68a609bf022bca15cf9a946d6559a90240682b2f5437e78e67b15d8ccb8b703967a48f069dda9236d0caa6d3e2fd4c5bd311f83420c29fa3f8416a4b720b59c04fec4ac68f92f8d485ecccf6b1a23aa4b05ae5c220a7a0d4b9388b19f5bb7df484611e2958c5d5179230a37b302ceaf4b52a1ae781017059f8cc011655acc2ada8cc6ac6276e7fa5223a93ab9a17287abe6e9158a983373bb3eea9c28f7f3d2c15268cf41822fda6a029396fa87986a11078e08f9ec6dada5ffaa7f0f437168a136d0c46d076181ec231df545dc7dc6d644c076f95ff427a295cf83d29e5d3a75fc347a3df9895e4042b34b6a38145f8465c0740137b7387d1138f5bda49bab5b03e1ffd2e051a83a7c599a764ff9d931e12509fdbf1e8479ffc74621051e136bf3a50c278a5ad3b5a40a8e1697603d381d8925032e8a3d7a01c38d966636e0d017ee4dc5ac590ae1b730664155fe25aa27d52a7aab8754d525fef530b15b82f8e8bac2218edc5fec27dbfa11446473a92152d03e9d7c7f54ed592a48f126f6e67cb72c9f4795125b1a8dbab6fdc761d0ba83ea903f21c5ddd522da45af655423bfcf012b812054612bd8309f0b31b6dd286d99925cc379904a684ea90239717f0cb13878b2cbf00ee81cd46b37dfa6d8e504101b58ae1b2a43d58e49f982fbc4e3ede6c38a6e223d7eb0cbd49bb6a8d07bfdaa2b3822d6999dfe9ba8b6629c0b692f3f199856e59412a6fbbd5dbae9b116c35f141bf4ace2c7575f733a920c2c3d5750a0d0bba467921a542e1ba3b50384b5b5da9da13bd211e204b9e731568190c035c5b5a319f96de5cddc2e64f6ce386f72ada6f175d025ce15ed00f1ac6bd2572ad214124879b8e9cd95e3573ef6773c5aae10eb2027735bf1d4be77d8a049b63968a4acda99fbfe648a4fbba5a2bc38d43504dcdb862b336488b3f3a09a91ea9ff1ba271575c101b0a14427e4504f1ad2a499e6b5d5c373a72edf3656f33b46c11434e27d019c30dd6924960ca2"; + +#[test] +fn pkcs1_v1_5_matches_bc_java_byte_for_byte() { + let sk = genuine_key(); + let sig256 = DeterministicSigningKey::::sign_deterministic( + &sk, b"hello", None, + ) + .unwrap(); + assert_eq!(sig256.to_vec(), hex_decode(BC_JAVA_SHA256_SIG).unwrap()); + + let sig384 = DeterministicSigningKey::::sign_deterministic( + &sk, b"hello", None, + ) + .unwrap(); + assert_eq!(sig384.to_vec(), hex_decode(BC_JAVA_SHA384_SIG).unwrap()); + + let sig512 = DeterministicSigningKey::::sign_deterministic( + &sk, b"hello", None, + ) + .unwrap(); + assert_eq!(sig512.to_vec(), hex_decode(BC_JAVA_SHA512_SIG).unwrap()); +} + +#[test] +fn pkcs1_v1_5_verifies_bc_java_signatures() { + let sk = genuine_key(); + let pk = RSA8192PublicKey::new(sk.n(), 0x10001).unwrap(); + let sig256: [u8; 1024] = hex_decode(BC_JAVA_SHA256_SIG).unwrap().try_into().unwrap(); + let sig384: [u8; 1024] = hex_decode(BC_JAVA_SHA384_SIG).unwrap().try_into().unwrap(); + let sig512: [u8; 1024] = hex_decode(BC_JAVA_SHA512_SIG).unwrap().try_into().unwrap(); + VerifyingKey::::verify(&pk, b"hello", None, &sig256) + .expect("BC Java's SHA-256 sig must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig384) + .expect("BC Java's SHA-384 sig must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig512) + .expect("BC Java's SHA-512 sig must verify"); +} + +#[test] +fn pss_verifies_bc_java_sha256_signature() { + let sk = genuine_key(); + let pk = RSA8192PublicKey::new(sk.n(), 0x10001).unwrap(); + let sig: [u8; 1024] = hex_decode(BC_JAVA_PSS_SHA256_SIG).unwrap().try_into().unwrap(); + VerifyingKey::::verify(&pk, b"hello", None, &sig) + .expect("BC Java's PSS/SHA-256 signature (default salt length = hLen) must verify"); +} + +/// Mutation testing found that fixed-salt PSS/SHA-256 signing, RNG-salted PSS/SHA-384 and /SHA-512 +/// signing, and PSS/SHA-384 and /SHA-512 verification's rejection paths were never +/// exercised at this modulus size (the old combined self-consistency test above only called one +/// salt source per hash and never asserted rejection): a whole-function-body mutant replacing any +/// of them with a constant still passed the whole suite. Split into six tests, one per (hash, salt +/// source) pairing, matching `rsa_2048_pss_sha384_sha512_tests.rs`'s own shape. +#[test] +fn pss_sha256_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA8192PublicKey::new(sk.n(), 0x10001).unwrap(); + let salt = [0x33u8; 32]; + let sig = + sign_with_salt!(RSASSA_PSS_SHA256, &sk, b"hello", salt).expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig) + .expect("must verify"); + assert!(VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err()); +} + +#[test] +fn pss_sha256_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA8192PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +#[test] +fn pss_sha384_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA8192PublicKey::new(sk.n(), 0x10001).unwrap(); + let salt = [0x11u8; 48]; + let sig = + sign_with_salt!(RSASSA_PSS_SHA384, &sk, b"hello", salt).expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig) + .expect("must verify"); + assert!(VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err()); +} + +#[test] +fn pss_sha384_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA8192PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +#[test] +fn pss_sha512_fixed_salt_round_trips() { + let sk = genuine_key(); + let pk = RSA8192PublicKey::new(sk.n(), 0x10001).unwrap(); + let salt = [0x22u8; 64]; + let sig = + sign_with_salt!(RSASSA_PSS_SHA512, &sk, b"hello", salt).expect("signing must succeed"); + VerifyingKey::::verify(&pk, b"hello", None, &sig) + .expect("must verify"); + assert!(VerifyingKey::::verify(&pk, b"goodbye", None, &sig).is_err()); +} + +#[test] +fn pss_sha512_rng_produces_fresh_salts_that_both_verify() { + let sk = genuine_key(); + let pk = RSA8192PublicKey::new(sk.n(), 0x10001).unwrap(); + let mut rng = DefaultRNG::default(); + let sig_a = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + let sig_b = SigningKey::::sign(&sk, &mut rng, b"hello", None) + .expect("signing must succeed"); + assert_ne!(sig_a, sig_b, "PSS is randomized: two signatures of the same message must differ"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_a) + .expect("sig_a must verify"); + VerifyingKey::::verify(&pk, b"hello", None, &sig_b) + .expect("sig_b must verify"); +} + +/// Every distinct `(result, flags)` combination in `filename`'s (single) test group, in file +/// order -- see this file's module docs for why this is a deliberate sample, not the full vector +/// set. +fn distinct_cases(tests: &[Value]) -> Vec<&Value> { + let mut seen = HashSet::new(); + let mut out = Vec::new(); + for test in tests { + let mut flags: Vec<&str> = + test["flags"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect(); + flags.sort_unstable(); + let key = (test["result"].as_str().unwrap().to_string(), flags); + if seen.insert(key) { + out.push(test); + } + } + out +} + +fn run_sampled_pkcs1_v1_5_verify_vectors( + filename: &str, + verify: impl Fn(&RSA8192PublicKey, &[u8], &[u8; 1024]) -> bool, + expected_sha: &str, +) { + let doc: Value = serde_json::from_str(&get_test_data(filename)).expect("valid JSON"); + let group = &doc["testGroups"][0]; + assert_eq!(group["sha"], expected_sha); + let n: [u64; 128] = limbs_from_hex(group["publicKey"]["modulus"].as_str().unwrap()); + let e = + u32::from_str_radix(group["publicKey"]["publicExponent"].as_str().unwrap(), 16).unwrap(); + let pk = RSA8192PublicKey::new(&n, e).unwrap(); + + let tests = group["tests"].as_array().unwrap(); + let sample = distinct_cases(tests); + assert!(sample.len() >= 10, "expected a rich sample of distinct cases, got {}", sample.len()); + + for test in sample { + let tc_id = test["tcId"].as_u64().unwrap(); + let msg = hex_decode(test["msg"].as_str().unwrap()).unwrap(); + let sig_bytes = hex_decode(test["sig"].as_str().unwrap()).unwrap(); + let flags: Vec<&str> = + test["flags"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).collect(); + + let Ok(sig): Result<[u8; 1024], _> = sig_bytes.try_into() else { + assert_ne!(test["result"], "valid", "tcId {tc_id}: wrong-length 'valid' signature"); + continue; + }; + let verified = verify(&pk, &msg, &sig); + match test["result"].as_str().unwrap() { + "valid" => assert!(verified, "tcId {tc_id}: expected valid, got invalid"), + "invalid" => assert!(!verified, "tcId {tc_id}: expected invalid, got valid"), + "acceptable" => { + assert_eq!(flags, vec!["MissingNull"], "tcId {tc_id}: unreviewed acceptable"); + assert!(verified, "tcId {tc_id}: MissingNull must be accepted"); + } + other => panic!("tcId {tc_id}: unknown result {other:?}"), + } + } +} + +#[test] +fn rsa_signature_8192_sha256_wycheproof_vectors_sampled() { + run_sampled_pkcs1_v1_5_verify_vectors( + "rsa_signature_8192_sha256_test.json", + |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }, + "SHA-256", + ); +} + +#[test] +fn rsa_signature_8192_sha384_wycheproof_vectors_sampled() { + run_sampled_pkcs1_v1_5_verify_vectors( + "rsa_signature_8192_sha384_test.json", + |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }, + "SHA-384", + ); +} + +#[test] +fn rsa_signature_8192_sha512_wycheproof_vectors_sampled() { + run_sampled_pkcs1_v1_5_verify_vectors( + "rsa_signature_8192_sha512_test.json", + |pk, msg, sig| { + VerifyingKey::::verify(pk, msg, None, sig).is_ok() + }, + "SHA-512", + ); +} + +// ---- bouncycastle_core trait conformance ------------------------------------------------------ + +fn fixed_keypair() -> Result<(RSA8192PublicKey, RSA8192PrivateKey), SignatureError> { + let sk = genuine_key(); + let pk = RSA8192PublicKey::new(sk.n(), 0x10001)?; + Ok((pk, sk)) +} + +/// `core-test-framework`'s full conformance suite for one pairing at this width; the rest get +/// [`trait_round_trip`], for the reason `rsa_4096_tests.rs` gives (an RSA-8192 signature is +/// slower still in a debug build). +#[test] +fn pkcs1_v1_5_sha256_trait_conformance_suite() { + TestFrameworkSignature::new(false).test_deterministic_signature::(fixed_keypair, false); +} + +#[test] +fn key_trait_boundary_conditions() { + TestFrameworkSignatureKeys::new() + .test_keys::(fixed_keypair); +} + +/// Sign one-shot and streamed, verify both, and reject a different message -- what a wrong width +/// constant in this size's aliases would break. PSS, through `SigningKey`. +fn pss_round_trip(pk: &RSA8192PublicKey, sk: &RSA8192PrivateKey) +where + RSA8192PrivateKey: SigningKey, + RSA8192PublicKey: VerifyingKey, +{ + let msg = b"RSA-8192 trait round trip"; + let mut rng = DefaultRNG::default(); + let sig = SigningKey::::sign(sk, &mut rng, msg, None).unwrap(); + VerifyingKey::::verify(pk, msg, None, &sig).unwrap(); + let mut signer = SigningKey::::create_signer(sk, &mut rng, None).unwrap(); + signer.do_update(&msg[..8]); + signer.do_update(&msg[8..]); + VerifyingKey::::verify(pk, msg, None, &signer.do_final().unwrap()).unwrap(); + assert!(VerifyingKey::::verify(pk, b"a different message", None, &sig).is_err()); +} + +/// As [`pss_round_trip`], for PKCS#1 v1.5 through `DeterministicSigningKey`. +fn pkcs1_round_trip(pk: &RSA8192PublicKey, sk: &RSA8192PrivateKey) +where + RSA8192PrivateKey: DeterministicSigningKey, + RSA8192PublicKey: VerifyingKey, +{ + let msg = b"RSA-8192 trait round trip"; + let sig = DeterministicSigningKey::::sign_deterministic(sk, msg, None).unwrap(); + VerifyingKey::::verify(pk, msg, None, &sig).unwrap(); + let mut signer = + DeterministicSigningKey::::create_signer_deterministic(sk, None).unwrap(); + signer.do_update(&msg[..8]); + signer.do_update(&msg[8..]); + assert_eq!(signer.do_final().unwrap(), sig); + assert!(VerifyingKey::::verify(pk, b"a different message", None, &sig).is_err()); +} + +#[test] +fn remaining_pairings_trait_round_trips() { + let (pk, sk) = fixed_keypair().unwrap(); + pkcs1_round_trip::(&pk, &sk); + pkcs1_round_trip::(&pk, &sk); + pss_round_trip::(&pk, &sk); + pss_round_trip::(&pk, &sk); + pss_round_trip::(&pk, &sk); +} + +/// The trait path reproduces BC Java's RSA-8192/PKCS#1 v1.5/SHA-256 signature byte for byte and +/// accepts BC Java's PSS signature -- the same cross-implementation check as the +/// tests above, through the key role traits. +#[test] +fn trait_matches_bc_java() { + let (pk, sk) = fixed_keypair().unwrap(); + let sig = DeterministicSigningKey::::sign_deterministic( + &sk, b"hello", None, + ) + .unwrap(); + assert_eq!(sig.to_vec(), hex_decode(BC_JAVA_SHA256_SIG).unwrap()); + let pss = hex_decode(BC_JAVA_PSS_SHA256_SIG).unwrap(); + VerifyingKey::::verify(&pk, b"hello", None, &pss).unwrap(); +} diff --git a/crypto/rsa/tests/rsasve_tests.rs b/crypto/rsa/tests/rsasve_tests.rs new file mode 100644 index 000000000..c2cc0f3b7 --- /dev/null +++ b/crypto/rsa/tests/rsasve_tests.rs @@ -0,0 +1,623 @@ +//! RSASVE (SP 800-56B Rev. 2 §7.2.1) as a KEM, at every modulus size that has one. +//! +//! Known-answer sources, all in `bc-test-data/crypto/rsa/`: +//! +//! * `acvp/KAS-IFC-SSC-Sp800-56Br2.json`: NIST ACVP-Server's sample KAS-IFC-SSC vector set +//! (`gen-val/json-files/KAS-IFC-SSC-Sp800-56Br2/internalProjection.json`). Its KAS1 (2048-bit) and +//! KAS2 (3072-bit) groups with CRT keys (`rsakpg1-crt`/`rsakpg2-crt`) carry RSASVE ciphertexts +//! with their secret values in both directions: `serverZ^iutE = serverC` and `iutZ^serverE = +//! iutC`. They exercise `decaps` directly, and `encaps_rng` through an RNG that emits the +//! vector's `Z`, since RSASVE.GENERATE's step 2.b turns the RNG's bytes straight into `z`. The +//! `failChangedZ` cases carry a `Z` inconsistent with `C`, which must not be recovered. +//! * `acvp/RSA-DecryptionPrimitive-Sp800-56Br2.json`: ACVP-Server's RSADP vector set +//! (`gen-val/json-files/RSA-DecryptionPrimitive-Sp800-56Br2/internalProjection.json`). Its CRT +//! groups (2048/3072/4096) are RSASVE.RECOVER's step 3.b, including the out-of-range +//! ciphertexts (`c <= 1` or `c >= n - 1`) that step 3.c must reject. The basic-format groups +//! (`keyMode: standard`, no CRT components) are skipped, since this crate has no `(n, d)` key +//! type. +//! * `openssl/openssl_rsasve_.json`: produced by `openssl/ossl_rsasve.c` against OpenSSL +//! 3.0.13's `EVP_PKEY_encapsulate` with `EVP_PKEY_CTX_set_kem_op(ctx, "RSASVE")`. Each file has +//! one freshly generated key (`e = 65537`) and five OpenSSL encapsulations `(C, Z)` under it. +//! These are the only third-party vectors at 8192 bits, and they are what the trait-conformance +//! suites below use as their fixed keys. + +use bouncycastle_core::errors::{KEMError, RNGError}; +use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey}; +use bouncycastle_core_test_framework::kem::{TestFrameworkKEM, TestFrameworkKEMKeys}; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rng::DefaultRNG; +use bouncycastle_rng::FixedSeedRNG; +use serde_json::Value; +use std::fs; +use std::path::Path; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/crypto/rsa"; +const TEST_DATA_PATH: &str = "../bc-test-data/crypto/rsa"; + +fn get_test_data(filename: &str) -> Value { + for dir in [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH] { + let path = format!("{dir}/{filename}"); + if Path::new(&path).exists() { + return serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap(); + } + } + panic!( + "bc-test-data not found (looked for {filename} in {TEST_DATA_PATH_RELATIVE:?} and \ + {TEST_DATA_PATH:?}); this suite requires it rather than skipping" + ); +} + +/// A big-endian hex integer as exactly `LEN` bytes, left-padded with zeros. ACVP drops leading +/// zero nibbles from some values (its `passLeadingZeroNibble` cases), so the input may be shorter. +fn be_bytes(hex: &str) -> [u8; LEN] { + let padded = if hex.len() % 2 == 1 { format!("0{hex}") } else { hex.to_string() }; + let bytes = hex_decode(&padded).expect("valid hex"); + assert!(bytes.len() <= LEN, "value wider than {LEN} bytes"); + let mut out = [0u8; LEN]; + out[LEN - bytes.len()..].copy_from_slice(&bytes); + out +} + +/// A big-endian hex integer as `L` little-endian limbs (the key constructors' representation). +fn limbs(hex: &str) -> [u64; L] { + let padded = if hex.len() % 2 == 1 { format!("0{hex}") } else { hex.to_string() }; + let bytes = hex_decode(&padded).expect("valid hex"); + assert!(bytes.len() <= 8 * L, "value wider than {L} limbs"); + let mut wide = vec![0u8; 8 * L - bytes.len()]; + wide.extend_from_slice(&bytes); + let mut out = [0u64; L]; + for i in 0..L { + let start = 8 * L - (i + 1) * 8; + out[i] = u64::from_be_bytes(wide[start..start + 8].try_into().unwrap()); + } + out +} + +/// An `RNG` whose stream is `bytes` repeated, reporting `strength`. +fn rng_emitting( + bytes: [u8; N], + strength: SecurityStrength, +) -> FixedSeedRNG<[u8; N]> { + let mut rng = FixedSeedRNG::new(bytes); + rng.set_security_strength(strength); + rng +} + +macro_rules! rsasve_suite { + ($module:ident, $size:ident, $bits:literal, $L:literal, $HALF:literal, $strength:expr, $weaker:expr) => { + mod $module { + use super::*; + use bouncycastle_rsa::$size::{CT_LEN, PK_LEN, RSASVE, SK_LEN, SS_LEN}; + + type PK = bouncycastle_rsa::rsasve::RsaKEMPublicKey<$L>; + type SK = bouncycastle_rsa::rsasve::RsaKEMPrivateKey<$L, $HALF>; + + fn openssl_vectors() -> Value { + get_test_data(&format!("openssl/openssl_rsasve_{}.json", $bits)) + } + + fn sk_from(v: &Value, p: &str, q: &str, dp: &str, dq: &str, qinv: &str) -> SK { + SK::from_crt_components( + &limbs::<$HALF>(v[p].as_str().unwrap()), + &limbs::<$HALF>(v[q].as_str().unwrap()), + &limbs::<$HALF>(v[dp].as_str().unwrap()), + &limbs::<$HALF>(v[dq].as_str().unwrap()), + &limbs::<$HALF>(v[qinv].as_str().unwrap()), + ) + .expect("vector CRT key must be accepted") + } + + fn openssl_keypair() -> Result<(PK, SK), KEMError> { + let v = openssl_vectors(); + let pk = PK::new(&limbs::<$L>(v["n"].as_str().unwrap()), 65537)?; + let sk = sk_from(&v, "p", "q", "dP", "dQ", "qInv"); + Ok((pk, sk)) + } + + #[test] + fn lengths_are_nlen() { + assert_eq!(CT_LEN, $bits / 8); + assert_eq!(SS_LEN, $bits / 8); + assert_eq!(PK_LEN, $bits / 8 + 4); + assert_eq!(SK_LEN, 5 * $bits / 16); + } + + #[test] + fn kem_trait_conformance_suite() { + // Not deterministic (fresh `Z` per call). "Implicitly rejecting" in the test + // framework's sense: a corrupted but in-range ciphertext recovers a different `Z` + // rather than an error, since RSASVE has no redundancy to check. + TestFrameworkKEM::new(false, true) + .test_kem::( + openssl_keypair, false, + ); + } + + #[test] + fn kem_key_trait_conformance_suite() { + TestFrameworkKEMKeys::new().test_keys::(openssl_keypair); + } + + #[test] + fn keys_round_trip_through_their_encoding() { + let (pk, sk) = openssl_keypair().unwrap(); + assert_eq!(PK::from_bytes(&pk.encode()).unwrap(), pk); + assert_eq!(SK::from_bytes(&sk.encode()).unwrap(), sk); + let mut pk_out = [0xAAu8; PK_LEN]; + assert_eq!(pk.encode_out(&mut pk_out), PK_LEN); + assert_eq!(pk_out, pk.encode()); + let mut sk_out = [0xAAu8; SK_LEN]; + assert_eq!(sk.encode_out(&mut sk_out), SK_LEN); + assert_eq!(sk_out, sk.encode()); + assert_eq!(pk.n(), sk.n()); + assert_eq!(pk.e(), 65537); + // `Display` is the wrapped signature key type's, unchanged. + let inner = bouncycastle_rsa::keys::RsaPublicKey::<$L>::new(pk.n(), 65537).unwrap(); + assert_eq!(format!("{pk}"), format!("{inner}")); + } + + /// OpenSSL encapsulated; this crate decapsulates. And the other way round: fed + /// OpenSSL's `Z` as its RNG output, `encaps_rng` must produce OpenSSL's `C` exactly + /// (RSAEP is deterministic in `z`). + #[test] + fn openssl_rsasve_vectors() { + let v = openssl_vectors(); + let (pk, sk) = openssl_keypair().unwrap(); + let encs = v["encapsulations"].as_array().unwrap(); + assert_eq!(encs.len(), 5); + for enc in encs { + let c = be_bytes::(enc["C"].as_str().unwrap()); + let z = be_bytes::(enc["Z"].as_str().unwrap()); + + let ss = RSASVE::decaps(&sk, &c).unwrap(); + assert_eq!(ss.ref_to_bytes(), &z[..]); + + let (ss, ct) = + RSASVE::encaps_rng(&pk, &mut rng_emitting(z, SecurityStrength::_256bit)) + .unwrap(); + assert_eq!(ct, c); + assert_eq!(ss.ref_to_bytes(), &z[..]); + } + } + + #[test] + fn shared_secret_is_labelled_with_the_modulus_strength() { + let (pk, sk) = openssl_keypair().unwrap(); + let (ss, ct) = RSASVE::encaps(&pk).unwrap(); + assert_eq!(ss.key_len(), SS_LEN); + assert_eq!(ss.key_type(), KeyType::CryptographicRandom); + assert_eq!(ss.security_strength(), $strength); + let ss2 = RSASVE::decaps(&sk, &ct).unwrap(); + assert_eq!(ss2.key_len(), SS_LEN); + assert_eq!(ss2.key_type(), KeyType::CryptographicRandom); + assert_eq!(ss2.security_strength(), $strength); + assert_eq!(ss, ss2); + } + + #[test] + fn encaps_rng_accepts_exactly_the_modulus_strength_and_rejects_below_it() { + let (pk, _) = openssl_keypair().unwrap(); + assert!( + RSASVE::encaps_rng(&pk, &mut rng_emitting([0x5Au8; CT_LEN], $strength)).is_ok() + ); + assert!(matches!( + RSASVE::encaps_rng(&pk, &mut rng_emitting([0x5Au8; CT_LEN], $weaker)), + Err(KEMError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)) + )); + } + + /// RSASVE.GENERATE step 2.c's boundaries, one draw at a time: `z` in {0, 1, n - 1, + /// all-ones} must be redrawn, and `z` in {2, n - 2} kept. + #[test] + fn generate_redraws_exactly_outside_one_to_n_minus_one() { + let (pk, sk) = openssl_keypair().unwrap(); + let n = be_bytes::(openssl_vectors()["n"].as_str().unwrap()); + let mut n_minus_1 = n; + n_minus_1[CT_LEN - 1] -= 1; // n is odd: no borrow + // Byte-wise `n - 2` needs no borrow only if `n`'s low byte is at least 3. + assert!(n[CT_LEN - 1] >= 3, "vector key's low byte makes n - 2 need a borrow"); + let mut n_minus_2 = n; + n_minus_2[CT_LEN - 1] -= 2; + let mut two = [0u8; CT_LEN]; + two[CT_LEN - 1] = 2; + let mut one = [0u8; CT_LEN]; + one[CT_LEN - 1] = 1; + let zero = [0u8; CT_LEN]; + let ones = [0xFFu8; CT_LEN]; + + for good in [two, n_minus_2] { + for bad in [zero, one, n_minus_1, n, ones] { + // The RNG emits `bad` then `good`: one rejected draw, then an accepted one. + let mut stream = [0u8; 2 * CT_LEN]; + stream[..CT_LEN].copy_from_slice(&bad); + stream[CT_LEN..].copy_from_slice(&good); + let (ss, ct) = RSASVE::encaps_rng( + &pk, + &mut rng_emitting(stream, SecurityStrength::_256bit), + ) + .unwrap(); + assert_eq!(ss.ref_to_bytes(), &good[..]); + assert_eq!(RSASVE::decaps(&sk, &ct).unwrap().ref_to_bytes(), &good[..]); + } + } + } + + #[test] + fn generate_gives_up_on_an_rng_that_never_lands_in_range() { + let (pk, _) = openssl_keypair().unwrap(); + for stuck in [[0xFFu8; CT_LEN], [0u8; CT_LEN]] { + assert!(matches!( + RSASVE::encaps_rng( + &pk, + &mut rng_emitting(stuck, SecurityStrength::_256bit) + ), + Err(KEMError::GenericError(_)) + )); + } + // One in-range draw after `MAX_GENERATE_ATTEMPTS - 1` rejected ones still succeeds: + // the cap is exactly `MAX_GENERATE_ATTEMPTS` draws, not one fewer. + const ATTEMPTS: usize = bouncycastle_rsa::rsasve::MAX_GENERATE_ATTEMPTS; + let mut stream = vec![0xFFu8; ATTEMPTS * CT_LEN]; + stream[(ATTEMPTS - 1) * CT_LEN..].fill(0x5A); + let seed: [u8; ATTEMPTS * CT_LEN] = stream.try_into().unwrap(); + let mut rng = FixedSeedRNG::new(seed); + let (ss, _) = RSASVE::encaps_rng(&pk, &mut rng).unwrap(); + assert_eq!(ss.ref_to_bytes(), &[0x5Au8; CT_LEN][..]); + } + + /// RSADP's step 1 range (§7.1.2.3): `1 < c < n - 1`. + #[test] + fn recover_rejects_exactly_outside_one_to_n_minus_one() { + let (_, sk) = openssl_keypair().unwrap(); + let n = be_bytes::(openssl_vectors()["n"].as_str().unwrap()); + let mut n_minus_1 = n; + n_minus_1[CT_LEN - 1] -= 1; + assert!(n[CT_LEN - 1] >= 3, "vector key's low byte makes n - 2 need a borrow"); + let mut n_minus_2 = n; + n_minus_2[CT_LEN - 1] -= 2; + let mut two = [0u8; CT_LEN]; + two[CT_LEN - 1] = 2; + let mut one = [0u8; CT_LEN]; + one[CT_LEN - 1] = 1; + + for bad in [[0u8; CT_LEN], one, n_minus_1, n, [0xFFu8; CT_LEN]] { + assert!(matches!( + RSASVE::decaps(&sk, &bad), + Err(KEMError::DecapsulationFailed) + )); + } + for good in [two, n_minus_2] { + assert!(RSASVE::decaps(&sk, &good).is_ok()); + } + } + + #[test] + fn recover_rejects_a_wrong_length_ciphertext() { + let (_, sk) = openssl_keypair().unwrap(); + for len in [0, CT_LEN - 1, CT_LEN + 1] { + assert!(matches!( + RSASVE::decaps(&sk, &vec![0x5Au8; len]), + Err(KEMError::LengthError(_)) + )); + } + } + + #[test] + fn public_key_validation() { + let v = openssl_vectors(); + let n = limbs::<$L>(v["n"].as_str().unwrap()); + // SP 800-56B Rev. 2 §6.2.1: 65,537 <= e. + assert!(PK::new(&n, 65537).is_ok()); + assert!(PK::new(&n, 65539).is_ok()); + for e in [3u32, 17, 65535] { + assert!(matches!(PK::new(&n, e), Err(KEMError::DecodingError(_)))); + } + // Still rejected for being even (RsaPublicKey's own check). + assert!(matches!(PK::new(&n, 65538), Err(KEMError::DecodingError(_)))); + let mut even_n = n; + even_n[0] &= !1; + assert!(matches!(PK::new(&even_n, 65537), Err(KEMError::DecodingError(_)))); + // n must be exactly $bits bits: its top bit set. + let mut short_n = n; + short_n[$L - 1] &= !(1 << 63); + assert!(matches!(PK::new(&short_n, 65537), Err(KEMError::DecodingError(_)))); + // The same checks apply on decoding. + let (pk, _) = openssl_keypair().unwrap(); + let mut bytes = pk.encode(); + bytes[0] &= 0x7F; + assert!(matches!(PK::from_bytes(&bytes), Err(KEMError::DecodingError(_)))); + let mut bytes = pk.encode(); + bytes[PK_LEN - 4..].copy_from_slice(&3u32.to_be_bytes()); + assert!(matches!(PK::from_bytes(&bytes), Err(KEMError::DecodingError(_)))); + } + + #[test] + fn private_key_validation() { + let v = openssl_vectors(); + // RsaPrivateKey's own checks come through as DecodingError: p == q. + assert!(matches!( + SK::from_crt_components( + &limbs::<$HALF>(v["p"].as_str().unwrap()), + &limbs::<$HALF>(v["p"].as_str().unwrap()), + &limbs::<$HALF>(v["dP"].as_str().unwrap()), + &limbs::<$HALF>(v["dP"].as_str().unwrap()), + &limbs::<$HALF>(v["qInv"].as_str().unwrap()), + ), + Err(KEMError::DecodingError(_)) + )); + // p = 3, q = 5, dP = dQ = qInv = 1: every RsaPrivateKey check passes, but + // n = 15 has its top bit clear. + let mut p = [0u64; $HALF]; + p[0] = 3; + let mut q = [0u64; $HALF]; + q[0] = 5; + let mut small = [0u64; $HALF]; + small[0] = 1; + assert!(matches!( + SK::from_crt_components(&p, &q, &small, &small, &small), + Err(KEMError::DecodingError(_)) + )); + let (_, sk) = openssl_keypair().unwrap(); + let mut bytes = sk.encode(); + bytes[..8 * $HALF].fill(0); // p = 0, which is even + assert!(matches!(SK::from_bytes(&bytes), Err(KEMError::DecodingError(_)))); + } + } + }; +} + +rsasve_suite!( + rsa_2048_rsasve, + rsa_2048, + 2048, + 32, + 16, + SecurityStrength::_112bit, + SecurityStrength::None +); +rsasve_suite!( + rsa_3072_rsasve, + rsa_3072, + 3072, + 48, + 24, + SecurityStrength::_128bit, + SecurityStrength::_112bit +); +rsasve_suite!( + rsa_4096_rsasve, + rsa_4096, + 4096, + 64, + 32, + SecurityStrength::_128bit, + SecurityStrength::_112bit +); +rsasve_suite!( + rsa_8192_rsasve, + rsa_8192, + 8192, + 128, + 64, + SecurityStrength::_192bit, + SecurityStrength::_128bit +); + +/// ACVP KAS-IFC-SSC's KAS1 group (2048-bit, CRT IUT key, IUT as responder): the server +/// encapsulated to the IUT's key. Also checks `encaps_rng` reproduces `serverC` from `serverZ`. +#[test] +fn acvp_kas_ifc_ssc_kas1_2048() { + use bouncycastle_rsa::rsa_2048::{CT_LEN, RSA2048KEMPrivateKey, RSA2048KEMPublicKey, RSASVE}; + let d = get_test_data("acvp/KAS-IFC-SSC-Sp800-56Br2.json"); + let group = d["testGroups"] + .as_array() + .unwrap() + .iter() + .find(|g| g["scheme"] == "KAS1" && g["keyGenerationMethod"] == "rsakpg1-crt") + .expect("KAS1 CRT group"); + assert_eq!(group["modulo"], 2048); + let (mut passed, mut failed) = (0, 0); + for t in group["tests"].as_array().unwrap() { + let s = |k: &str| t[k].as_str().unwrap(); + let sk = RSA2048KEMPrivateKey::from_crt_components( + &limbs(s("iutP")), + &limbs(s("iutQ")), + &limbs(s("iutDmp1")), + &limbs(s("iutDmq1")), + &limbs(s("iutIqmp")), + ) + .unwrap(); + let e = u32::from_str_radix(s("iutE"), 16).unwrap(); + let pk = RSA2048KEMPublicKey::new(&limbs(s("iutN")), e).unwrap(); + let c = be_bytes::(s("serverC")); + let z = be_bytes::(s("serverZ")); + let recovered = RSASVE::decaps(&sk, &c).unwrap(); + if t["testPassed"].as_bool().unwrap() { + assert_eq!(recovered.ref_to_bytes(), &z[..], "tcId {}", t["tcId"]); + let (_, ct) = + RSASVE::encaps_rng(&pk, &mut rng_emitting(z, SecurityStrength::_256bit)).unwrap(); + assert_eq!(ct, c, "tcId {}", t["tcId"]); + passed += 1; + } else { + assert_ne!(recovered.ref_to_bytes(), &z[..], "tcId {}", t["tcId"]); + failed += 1; + } + } + assert_eq!((passed, failed), (4, 1)); +} + +/// ACVP KAS-IFC-SSC's KAS2 CRT group (3072-bit, both parties' keys CRT): one RSASVE in each +/// direction per case. +#[test] +fn acvp_kas_ifc_ssc_kas2_3072() { + use bouncycastle_rsa::rsa_3072::{CT_LEN, RSA3072KEMPrivateKey, RSA3072KEMPublicKey, RSASVE}; + let d = get_test_data("acvp/KAS-IFC-SSC-Sp800-56Br2.json"); + let group = d["testGroups"] + .as_array() + .unwrap() + .iter() + .find(|g| g["scheme"] == "KAS2" && g["keyGenerationMethod"] == "rsakpg2-crt") + .expect("KAS2 CRT group"); + assert_eq!(group["modulo"], 3072); + let (mut passed, mut failed) = (0, 0); + for t in group["tests"].as_array().unwrap() { + let s = |k: &str| t[k].as_str().unwrap(); + let key = |who: &str| { + let sk = RSA3072KEMPrivateKey::from_crt_components( + &limbs(s(&format!("{who}P"))), + &limbs(s(&format!("{who}Q"))), + &limbs(s(&format!("{who}Dmp1"))), + &limbs(s(&format!("{who}Dmq1"))), + &limbs(s(&format!("{who}Iqmp"))), + ) + .unwrap(); + let e = u32::from_str_radix(s(&format!("{who}E")), 16).unwrap(); + let pk = RSA3072KEMPublicKey::new(&limbs(s(&format!("{who}N"))), e).unwrap(); + (pk, sk) + }; + let (iut_pk, iut_sk) = key("iut"); + let (server_pk, server_sk) = key("server"); + // (recipient pk, recipient sk, C, Z): serverC was made to the IUT's key, iutC to the + // server's. + let directions = [ + (&iut_pk, &iut_sk, s("serverC"), s("serverZ")), + (&server_pk, &server_sk, s("iutC"), s("iutZ")), + ]; + let ok = t["testPassed"].as_bool().unwrap(); + for (pk, sk, c, z) in directions { + let c = be_bytes::(c); + let z = be_bytes::(z); + let recovered = RSASVE::decaps(sk, &c).unwrap(); + if ok { + assert_eq!(recovered.ref_to_bytes(), &z[..], "tcId {}", t["tcId"]); + let (_, ct) = + RSASVE::encaps_rng(pk, &mut rng_emitting(z, SecurityStrength::_256bit)) + .unwrap(); + assert_eq!(ct, c, "tcId {}", t["tcId"]); + } else { + assert_ne!(recovered.ref_to_bytes(), &z[..], "tcId {}", t["tcId"]); + } + } + if ok { + passed += 1; + } else { + failed += 1; + } + } + assert_eq!((passed, failed), (4, 1)); +} + +/// ACVP's SP 800-56B Rev. 2 RSADP vectors, CRT groups: RSASVE.RECOVER is RSADP plus I2BS, so +/// `decaps(ct)` must be `pt` at `nLen` bytes, or `DecapsulationFailed` for the out-of-range +/// ciphertexts ACVP marks as failing. +macro_rules! acvp_rsadp { + ($name:ident, $size:ident, $bits:literal, $L:literal, $HALF:literal) => { + #[test] + fn $name() { + use bouncycastle_rsa::$size::{CT_LEN, RSASVE}; + type SK = bouncycastle_rsa::rsasve::RsaKEMPrivateKey<$L, $HALF>; + let d = get_test_data("acvp/RSA-DecryptionPrimitive-Sp800-56Br2.json"); + let group = d["testGroups"] + .as_array() + .unwrap() + .iter() + .find(|g| g["keyMode"] == "crt" && g["modulo"] == $bits) + .expect("CRT group"); + let (mut passed, mut failed) = (0, 0); + for t in group["tests"].as_array().unwrap() { + let s = |k: &str| t[k].as_str().unwrap(); + let sk = SK::from_crt_components( + &limbs(s("p")), + &limbs(s("q")), + &limbs(s("dmp1")), + &limbs(s("dmq1")), + &limbs(s("iqmp")), + ) + .unwrap(); + let ct = be_bytes::(s("ct")); + if t["testPassed"].as_bool().unwrap() { + let pt = be_bytes::(s("pt")); + assert_eq!( + RSASVE::decaps(&sk, &ct).unwrap().ref_to_bytes(), + &pt[..], + "tcId {}", + t["tcId"] + ); + passed += 1; + } else { + assert!( + matches!(RSASVE::decaps(&sk, &ct), Err(KEMError::DecapsulationFailed)), + "tcId {}", + t["tcId"] + ); + failed += 1; + } + } + assert_eq!(passed + failed, 15); + assert!(failed > 0, "the group's out-of-range cases must have been exercised"); + } + }; +} + +acvp_rsadp!(acvp_rsadp_crt_2048, rsa_2048, 2048, 32, 16); +acvp_rsadp!(acvp_rsadp_crt_3072, rsa_3072, 3072, 48, 24); +acvp_rsadp!(acvp_rsadp_crt_4096, rsa_4096, 4096, 64, 32); + +/// `RSASVE::keygen`'s pairs are usable KEM keys (a genuine generation, at the smallest size +/// only: key generation is the slow part), and `keygen` enforces the RNG strength. +#[test] +fn keygen_produces_working_kem_keys() { + use bouncycastle_rsa::rsa_2048::RSASVE; + let (pk, sk) = RSASVE::keygen(&mut DefaultRNG::default()).unwrap(); + assert_eq!(pk.e(), 65537); + assert_eq!(pk.n(), sk.n()); + let (ss, ct) = RSASVE::encaps(&pk).unwrap(); + assert_eq!(RSASVE::decaps(&sk, &ct).unwrap(), ss); + + // A stuck RNG exhausts FIPS 186-5 A.1.3's candidate limit (see `keygen_tests.rs`'s + // `keygen_gives_up_on_a_stuck_rng`), and the KEM keygen passes that error through + // as `GenericError` with key generation's own message, not a generic one. + let mut stuck = FixedSeedRNG::new([0x42u8; 64]); + match RSASVE::keygen(&mut stuck) { + Err(KEMError::GenericError(msg)) => assert!(msg.contains("candidate limit"), "{msg}"), + other => panic!("expected GenericError, got {other:?}"), + } + + let mut weak = FixedSeedRNG::new([0x5Au8; 64]); + weak.set_security_strength(SecurityStrength::None); + assert!(matches!( + RSASVE::keygen(&mut weak), + Err(KEMError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)) + )); +} + +/// `RSASVE::keygen` at the larger sizes, following `keygen_tests.rs`: 3072 runs by default, and +/// 4096/8192 are `#[ignore]`d for the same reason as there. +macro_rules! kem_keygen_round_trip { + ($name:ident, $size:ident $(, $ignore:meta)?) => { + #[test] + $(#[$ignore])? + fn $name() { + use bouncycastle_rsa::$size::RSASVE; + let (pk, sk) = RSASVE::keygen(&mut DefaultRNG::default()).unwrap(); + assert_eq!(pk.e(), 65537); + assert_eq!(pk.n(), sk.n()); + let (ss, ct) = RSASVE::encaps(&pk).unwrap(); + assert_eq!(RSASVE::decaps(&sk, &ct).unwrap(), ss); + } + }; +} + +kem_keygen_round_trip!(kem_keygen_3072_round_trips, rsa_3072); +kem_keygen_round_trip!( + kem_keygen_4096_round_trips, + rsa_4096, + ignore = "tens of minutes in a debug build: run with --release --ignored" +); +kem_keygen_round_trip!( + kem_keygen_8192_round_trips, + rsa_8192, + ignore = "tens of minutes in a debug build: run with --release --ignored" +); diff --git a/crypto/sm2/Cargo.toml b/crypto/sm2/Cargo.toml new file mode 100644 index 000000000..82a07918c --- /dev/null +++ b/crypto/sm2/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "bouncycastle-sm2" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-ec.workspace = true +bouncycastle-rng.workspace = true +bouncycastle-sm3.workspace = true +bouncycastle-utils.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +criterion.workspace = true + +[[bench]] +name = "sm2_benches" +harness = false diff --git a/crypto/sm2/benches/sm2_benches.rs b/crypto/sm2/benches/sm2_benches.rs new file mode 100644 index 000000000..dbe37e82c --- /dev/null +++ b/crypto/sm2/benches/sm2_benches.rs @@ -0,0 +1,37 @@ +use bouncycastle_core::signature_key::{SigningKey, VerifyingKey}; +use bouncycastle_rng::DefaultRNG; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_sm2::keys::keygen; + +const MSG: &[u8] = b"a representative message for benchmarking SM2"; +const ID: &[u8] = b"benchmark-identity@example.com"; + +fn bench_sm2(c: &mut Criterion) { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, Some(ID)).unwrap(); + + let mut group = c.benchmark_group("sm2"); + group.bench_function("keygen", |bencher| { + bencher.iter(|| black_box(keygen(&mut DefaultRNG::default()).unwrap())) + }); + group.bench_function("sign", |bencher| { + bencher.iter(|| { + black_box( + black_box(&sk) + .sign(&mut DefaultRNG::default(), black_box(MSG), Some(black_box(ID))) + .unwrap(), + ) + }) + }); + group.bench_function("verify", |bencher| { + bencher.iter(|| { + black_box(&pk).verify(black_box(MSG), Some(black_box(ID)), black_box(&sig)).unwrap() + }) + }); + group.finish(); +} + +criterion_group!(benches, bench_sm2); +criterion_main!(benches); diff --git a/crypto/sm2/src/extra_bits.rs b/crypto/sm2/src/extra_bits.rs new file mode 100644 index 000000000..42efdf956 --- /dev/null +++ b/crypto/sm2/src/extra_bits.rs @@ -0,0 +1,87 @@ +//! "Extra random bits" reduction: turns a DRBG output wider than `n` into a value in `[1, n-1]` +//! (the per-message secret `k`) or `[1, n-2]` (the private key `dA`) with negligible bias, the +//! same technique `bouncycastle-ecdsa`'s `extra_bits` module uses for P-256 (and every brainpool +//! curve on this branch). GB/T 32918.2-2016 §6.1 step A3 draws `k` from `[1, n-1]` and GB/T +//! 32918.1-2016 §6.1 a) draws `dA` from `[1, n-2]`; both say only to use "a random number +//! generator" (`draft-shen-sm2-ecdsa-02` likewise, and it states no range for `dA`) without +//! specifying a DRBG-to-scalar conversion, so this reuses FIPS 186-5 Appendix A.4.1's method (the +//! same bias-bound methodology this workspace already applies to every other curve) rather than a +//! plain modular reduction, which would be measurably biased for a `DRBG output length == n's bit +//! length` request. +//! +//! # Why SM2 needs this at all +//! +//! SM2's `n` sits `2^224` below `2^256` (`2^256 - n = 2^224 * 1.000000`, to six places) -- the +//! same distance P-256's `n` keeps from its power of two (`2^224 * 0.99999999977`), and for the +//! same reason not close enough: a 256-bit DRBG output reduced mod `n - 1` would land in `[0, +//! 2^256 - n)` about twice as often as anywhere else, which is the bias FIPS 186-5's Table A.2 +//! gives P-256 its 288/352-bit entries to dilute. (The brainpool curves need the extra bits for a +//! different reason -- their `n` is nowhere near a power of two -- while P-384, P-521 and +//! secp256k1 need none for bias, their `n` being within a relative `2^-128` or closer of one, and +//! draw them only because FIPS 186-5 Appendix A.3.1 requires `t >= 64` for a per-message +//! secret.) FIPS 186-5 Appendix A.4.1 step 2's bias-bound check (`2ρ(1-ρ)(n-1) > ε·N`, `ε = +//! 2^-64`) was evaluated directly (in Python, against `n`'s actual value from +//! `draft-shen-sm2-ecdsa-02` Appendix D) for `N = 2^l` at increasing `l`: `l = 256` (matching +//! `n`'s own bit length) fails the check, and the smallest passing `l` is `319`; this crate uses +//! `l = 320` (40 bytes, byte-aligned) as `crate::keys::EXTRA_BITS_DRBG_OUTPUT_LEN`. +//! +//! # Two moduli +//! +//! Appendix A.4.1 steps 3-5 are `x mod (n-1)`, then `+ 1`: an output in `[1, n-1]`, the range +//! `k` needs. `dA`'s range is one smaller, so [`reduce_wide_bits_mod_n_minus_2`] runs the same +//! steps with modulus `n - 2` and lands in `[1, n-2]`. The bias-bound check above re-evaluated +//! with `n - 2` in place of `n - 1` gives the same answer -- `l = 319` is the smallest passing +//! length and `l = 320` passes with the same `2^-31` margin -- so one DRBG output length serves +//! both. +//! +//! The reduction itself is [`bouncycastle_ec::barrett`]'s constant-time Barrett reduction (see +//! `bouncycastle_ecdsa`'s `extra_bits` docs for the constant-time argument); `extra_bits_tests.rs` +//! pins known answers computed in Python for both moduli and cross-checks each against the +//! bit-serial algorithm this module used to implement, kept there as an independent reference. + +use bouncycastle_ec::barrett; +use bouncycastle_ec::nat; +use bouncycastle_ec::sm2_scalar::{N_LIMBS, Sm2Scalar}; + +/// `n - 1`, little-endian `u64` limbs. `N_LIMBS[0]` ends in `0x23`, so neither this subtraction +/// nor [`N_MINUS_2_LIMBS`]'s borrows out of the low limb. +pub(crate) const N_MINUS_1_LIMBS: [u64; 4] = [N_LIMBS[0] - 1, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]]; + +/// `n - 2`, little-endian `u64` limbs. +const N_MINUS_2_LIMBS: [u64; 4] = [N_LIMBS[0] - 2, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]]; + +/// `floor(2^512 / (n-1)) - 2^256`: the low limbs of Barrett's `mu` for `m = n - 1` (whose top +/// limb is `mu`'s only other bit -- see [`bouncycastle_ec::barrett`]), computed in Python from +/// draft-shen-sm2-ecdsa-02 Appendix D's `n`. +const MU_LOW_LIMBS_N_MINUS_1: [u64; 4] = + [0x12ac6361f15149a1, 0x8dfc2096fa323c01, 0x0000000100000001, 0x0000000100000001]; + +/// `floor(2^512 / (n-2)) - 2^256`, the same for `m = n - 2`: only the low limb differs. +const MU_LOW_LIMBS_N_MINUS_2: [u64; 4] = + [0x12ac6361f15149a2, 0x8dfc2096fa323c01, 0x0000000100000001, 0x0000000100000001]; + +/// FIPS 186-5 Appendix A.4.1 steps 3-5 for `n` = the curve order: reduces the big-endian bit string +/// `bytes` modulo `n-1` and adds `1`, landing in `[1, n-1]` -- the range of the per-message +/// secret `k` (GB/T 32918.2-2016 §6.1 step A3). `bytes` may be up to `63` bytes -- `2^504 < (n-1) +/// * 2^256`, Barrett's precondition -- which covers the 40-byte DRBG output this crate draws with +/// room to spare; a longer input is a programming error and panics. +pub fn reduce_wide_bits_mod_n_minus_1(bytes: &[u8]) -> Sm2Scalar { + reduce_wide_bits(bytes, &N_MINUS_1_LIMBS, &MU_LOW_LIMBS_N_MINUS_1) +} + +/// The same steps with modulus `n-2`: `bytes mod (n-2)`, plus `1`, landing in `[1, n-2]` -- the +/// range of the private key `dA` (GB/T 32918.1-2016 §6.1 a)). Same input width limit as +/// [`reduce_wide_bits_mod_n_minus_1`], since `2^504 < (n-2) * 2^256` too. +pub fn reduce_wide_bits_mod_n_minus_2(bytes: &[u8]) -> Sm2Scalar { + reduce_wide_bits(bytes, &N_MINUS_2_LIMBS, &MU_LOW_LIMBS_N_MINUS_2) +} + +/// `bytes mod m + 1` for either modulus, as a scalar. `m`'s top bit is set and `2^504 < m * 2^256` +/// for both, so the 63-byte limit is Barrett's `t < m * 2^256` precondition in both cases. +fn reduce_wide_bits(bytes: &[u8], m: &[u64; 4], mu_low: &[u64; 4]) -> Sm2Scalar { + assert!(bytes.len() < 64, "reduce_wide_bits given {} bytes, more than 63", bytes.len()); + let t = barrett::limbs_from_be_bytes::<8>(bytes); + let reduced = barrett::reduce::<4, 8, 5>(&t, m, mu_low); + let (plus_one, _) = nat::add(&reduced, &[1, 0, 0, 0]); + Sm2Scalar::from_limbs(plus_one) +} diff --git a/crypto/sm2/src/keys.rs b/crypto/sm2/src/keys.rs new file mode 100644 index 000000000..b3c023c11 --- /dev/null +++ b/crypto/sm2/src/keys.rs @@ -0,0 +1,225 @@ +//! SM2 key types and key-pair generation (GB/T 32918.1-2016 §6.1, which GB/T 32918.2-2016 §5.3 +//! defers to for the signer's key pair). Shaped identically to `bouncycastle-ecdsa`'s per-curve +//! `keys_*.rs` modules -- see that crate's `keys_bp256r1` module's docs for the full reasoning -- +//! with SM2's own field/scalar types and the wide-DRBG-output reduction from +//! [`crate::extra_bits`] substituted. §6.1 a) says only that `dA` is an integer in `[1, n-2]` +//! produced "with a random number generator" (`draft-shen-sm2-ecdsa-02` says the same without a +//! range), without specifying a DRBG-to-scalar conversion; this crate uses the same FIPS 186-5 +//! Appendix A.4.1 "extra random bits" method as every curve in `bouncycastle-ecdsa`, for the same +//! reason (see [`crate::extra_bits`]'s docs for the bias-bound derivation specific to SM2's `n`). + +use crate::extra_bits::{N_MINUS_1_LIMBS, reduce_wide_bits_mod_n_minus_2}; +use bouncycastle_core::errors::{RNGError, SignatureError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::nat; +use bouncycastle_ec::sm2::Sm2FieldElement; +use bouncycastle_ec::sm2_comb::comb_multiply_base_point; +use bouncycastle_ec::sm2_scalar::{Sm2Scalar, Sm2ScalarField}; +use bouncycastle_ec::sm2_sec1; +use bouncycastle_utils::secret::Secret; +use core::fmt; +use core::fmt::{Debug, Display, Formatter}; + +/// Encoded length of an SM2 private key: SEC 1 §2.3.7 integer-to-octet-string of `dA`, 32 bytes. +pub const SK_LEN: usize = 32; + +/// Encoded length of an SM2 public key in the canonical uncompressed form (SEC 1 §2.3.3, `04 || X +/// || Y`); [`SM2PublicKey::from_bytes`] also accepts the 33-byte compressed form. +pub const PK_LEN: usize = 65; + +/// Requested output length, in bytes, from the DRBG for key generation and for the per-signature +/// secret `k` (GB/T 32918.2-2016 §6.1 step A3): `320` bits (40 bytes) -- derived directly +/// from FIPS 186-5 Appendix A.4.1's own bias-bound check for SM2's `n` (see [`crate::extra_bits`]'s +/// docs), not copied from any other curve's table entry. +pub(crate) const EXTRA_BITS_DRBG_OUTPUT_LEN: usize = 40; + +/// An SM2 private key: `dA` in `[1, n-2]` (GB/T 32918.1-2016 §6.1 a); the draft states no range), +/// held in [`bouncycastle_utils::secret::Secret`] via [`Sm2Scalar`]. +/// +/// # Why the public key is carried alongside `dA` +/// +/// Unlike ECDSA, *every* SM2 signing operation needs the signer's own public key: GB/T +/// 32918.2-2016 §5.5's `ZA = SM3(ENTLA || IDA || a || b || xG || yG || xA || yA)` mixes `PA`'s +/// affine coordinates into the +/// message hash, so `sign` cannot proceed without them. Deriving `PA` on each call means a second +/// fixed-base scalar multiplication per signature -- as expensive as the `[k]G` the signature +/// actually needs, i.e. double the cost of signing, and measurably so: signing used to cost about +/// twice what key generation did, and slightly more than *verification*, which is backwards for +/// any signature scheme. +/// +/// So `PA` is computed once, when the key is created ([`keygen`], which has it for free) +/// or loaded ([`SignaturePrivateKey::from_bytes`], which pays one multiplication once instead of +/// one per signature), and kept here. The cost is [`core::mem::size_of`] for this struct rising +/// from 32 bytes to 96 -- `PA` is two field elements -- which the crate docs' Memory Footprint +/// table records. `PA` is public data and is deliberately *not* wrapped in +/// [`bouncycastle_utils::secret::Secret`]; only `dA` is. +/// +/// # Why `(1 + dA)^-1` is carried too +/// +/// GB/T 32918.2-2016 §6.1 step A6 divides by `1 + dA`, and that inverse depends on the +/// key alone, not on the message or `k`. Recomputing it per signature was a constant-time +/// Fermat inversion each time -- roughly the same cost as `to_affine`'s field inversion, about a +/// tenth of a signature on P-256-class hardware -- for a value that never changes. It is computed +/// once, in [`SM2PrivateKey::from_validated_scalar`], and held in [`Sm2Scalar`], the same +/// [`bouncycastle_utils::secret::Secret`] wrapper as `dA`: anyone holding `(1 + dA)^-1` recovers +/// `dA` by inverting and subtracting one, so it is exactly as sensitive. That is 32 more bytes in +/// memory (128 in all, per the crate docs' Memory Footprint table). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SM2PrivateKey { + d: Sm2Scalar, + /// `PA = [dA]G`, cached at construction; see the type's docs. + pk: SM2PublicKey, + /// `(1 + dA)^-1 mod n`, cached at construction; see the type's docs. + one_plus_d_inv: Sm2Scalar, +} + +impl SM2PrivateKey { + /// Builds the key pair from an already-validated `dA` in `[1, n-2]`, computing `PA = [dA]G`. + /// The single place `SM2PrivateKey`'s invariant (`pk` really is `[d]G`) is established. + fn from_validated_scalar(d: Sm2Scalar) -> Self { + let q = comb_multiply_base_point(&d); + // dA is in [1, n-2] by construction (every caller validates first) and G has prime order n + // (h = 1), so [dA]G is never the identity. + let (x, y) = q.to_affine().expect("[dA]G is never infinity for dA in [1, n-2]"); + + // (1 + dA)^-1, once per key rather than once per signature (see the type's docs). The + // three scalar-field intermediates reveal dA and are scrubbed, the same way sign_with_k + // scrubs its own; the cached value itself goes into a Secret. 1 + dA is at most n - 1, + // since dA <= n - 2, so the inverse is never of 0. + let mut d_field = Sm2ScalarField::from_secret(&d); + let mut one_plus_d = Sm2ScalarField::ONE.add(&d_field); + let mut inverse = one_plus_d.invert(); + let one_plus_d_inv = Sm2Scalar::from_limbs(inverse.to_limbs()); + d_field.zeroize(); + one_plus_d.zeroize(); + inverse.zeroize(); + + Self { d, pk: SM2PublicKey { x, y }, one_plus_d_inv } + } + + /// The wrapped scalar, for this crate's own sign implementation to compute with. + pub(crate) fn scalar(&self) -> &Sm2Scalar { + &self.d + } + + /// The cached `(1 + dA)^-1 mod n` (see the type's docs), for step A6 of signing. + pub(crate) fn one_plus_d_inv(&self) -> &Sm2Scalar { + &self.one_plus_d_inv + } + + /// The matching public key `PA = [dA]G`. Free: it was computed when this key was created or + /// loaded (see the type's docs), not recomputed here. + /// + /// For the CLI's `PkFromSk`/`CheckConsistency` actions, which need it without having generated + /// the pair together -- see `bouncycastle_ecdsa::keys_common::DerivePublicKey`'s docs for the + /// shape this mirrors (kept as a plain inherent method here rather than a shared trait, since + /// SM2's own CLI command needs the extra `IDA` argument every other curve's doesn't, so it was + /// never going to share a single generic command function with them anyway). + pub fn derive_pk(&self) -> SM2PublicKey { + self.pk + } +} + +impl SignaturePrivateKey for SM2PrivateKey { + fn encode(&self) -> [u8; SK_LEN] { + self.d.to_be_bytes() + } + + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { + out.fill(0); + *out = self.d.to_be_bytes(); + SK_LEN + } + + fn from_bytes(bytes: &[u8]) -> Result { + let array: [u8; SK_LEN] = bytes + .try_into() + .map_err(|_| SignatureError::DecodingError("SM2 private key must be 32 bytes"))?; + // GB/T 32918.1-2016 S6.1 a): dA is in [1, n-2], checked on the raw big-endian value + // *before* any reduction. `Sm2Scalar::from_be_bytes` reduces mod n, so checking afterwards + // would accept an out-of-range encoding as a different, perfectly valid key: `dA = n + 1` + // would load as `dA = 1`, giving one key two encodings and silently treating malformed + // input as well-formed. Comparing against the fixed public values 0 and n - 1 is a + // one-time validation of caller-supplied bytes at load time, not a computation performed + // repeatedly on a secret intermediate value, so branching on it leaks nothing beyond what + // the caller already knows from having supplied these exact bytes. + let limbs = sm2_sec1::limbs_from_be_bytes(&array); + // borrow == 1 iff limbs < n - 1, i.e. limbs <= n - 2. + let (_, borrow) = nat::sub(&limbs, &N_MINUS_1_LIMBS); + if limbs == [0; 4] || borrow != 1 { + return Err(SignatureError::DecodingError("SM2 private key must be in [1, n-2]")); + } + Ok(Self::from_validated_scalar(Sm2Scalar::from_limbs(limbs))) + } +} + +/// An SM2 public key: `PA = [dA]G` (GB/T 32918.1-2016 §6.1 b)), stored as affine coordinates. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SM2PublicKey { + pub(crate) x: Sm2FieldElement, + pub(crate) y: Sm2FieldElement, +} + +impl SignaturePublicKey for SM2PublicKey { + fn encode(&self) -> [u8; PK_LEN] { + sm2_sec1::encode_uncompressed(&self.x, &self.y) + } + + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { + out.fill(0); + *out = self.encode(); + PK_LEN + } + + /// Accepts both SEC 1 §2.3.3 encodings (33-byte compressed, 65-byte uncompressed), fully + /// validated per [`sm2_sec1::decode`]'s docs (partial validation is full validation here: SM2's + /// cofactor is `h = 1`, per that module's docs). + fn from_bytes(bytes: &[u8]) -> Result { + let (x, y) = sm2_sec1::decode(bytes).ok_or(SignatureError::DecodingError( + "invalid or out-of-range SEC 1 SM2 point encoding", + ))?; + Ok(Self { x, y }) + } +} + +impl Display for SM2PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "SM2PublicKey {{ {:x?} }}", self.encode()) + } +} + +impl SM2PublicKey { + /// The public key's affine `x`, `y` coordinates -- for [`crate::za`]'s `ZA` computation, which + /// needs them directly rather than through the SEC 1 encoding. + pub(crate) fn affine(&self) -> (Sm2FieldElement, Sm2FieldElement) { + (self.x, self.y) + } +} + +/// As [`keygen`], but sources the DRBG output from the caller-provided RNG, which must offer +/// a security strength of at least 128 bits: GB/T 32918.2-2016 §5.4.3 asks only for an approved +/// random number generator and the draft says no more (see the module docs on why key generation +/// borrows FIPS 186-5's method wholesale), so this follows that +/// method's own Appendix A.2.1 step 3, requiring a DRBG strength "not less than" the one SP +/// 800-57 Part 1 Rev. 5's Table 2 associates with SM2's 256-bit curve order (`f = 256-383` in +/// that table's ECC column, giving 128-bit security). +pub fn keygen(rng: &mut dyn RNG) -> Result<(SM2PublicKey, SM2PrivateKey), SignatureError> { + // FIPS 186-5 Appendix A.2.1 step 3: the DRBG must offer at least the security strength + // SM2's 256-bit curve order calls for (SP 800-57 Part 1 Rev. 5, Table 2: 128 bits). + if rng.security_strength() < SecurityStrength::_128bit { + return Err(SignatureError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)); + } + + // Raw DRBG output, reduced below into the private key / per-message secret: held in + // `Secret` so it is scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + let d = reduce_wide_bits_mod_n_minus_2(&*extra_bits); + + // d is in [1, n-2] by construction (see reduce_wide_bits_mod_n_minus_2), which is what + // `from_validated_scalar` assumes; it computes PA = [d]G once, and the pair shares that value. + let sk = SM2PrivateKey::from_validated_scalar(d); + + Ok((sk.derive_pk(), sk)) +} diff --git a/crypto/sm2/src/lib.rs b/crypto/sm2/src/lib.rs new file mode 100644 index 000000000..29dddfda8 --- /dev/null +++ b/crypto/sm2/src/lib.rs @@ -0,0 +1,140 @@ +//! SM2: the Chinese national-standard elliptic-curve Digital Signature Algorithm (GB/T +//! 32918.2-2016, mirrored by the IETF informational draft `draft-shen-sm2-ecdsa-02`, whose §5 +//! is the same algorithm). This crate implements only the Digital Signature Algorithm -- key +//! generation ([`keys`]) and signing/verification ([`sm2::SM2`]) -- built on +//! [`bouncycastle_ec::sm2`]'s field/scalar/point arithmetic, SEC 1 encoding, and comb/wNAF +//! multipliers, and [`bouncycastle_sm3`] for the hash (see [`sm2`]'s docs on why only SM3). +//! +//! # Scope +//! +//! `draft-shen-sm2-ecdsa-02` also describes an SM2 Key Exchange Protocol (§6) and an SM2 Public Key +//! Encryption scheme (§7); neither is implemented here. SM2's Digital Signature Algorithm is *not* +//! ECDSA -- it has its own signing equation and its own identity-binding `ZA` digest (see +//! [`za`]'s docs) -- despite both being built on the same family of elliptic-curve arithmetic that +//! `bouncycastle-ecdsa`'s curves use. +//! +//! # `ctx` carries the signer's identity +//! +//! Every SM2 signing/verification operation needs the signer's identity octet string `IDA`, which +//! `ZA` binds into the hash (GB/T 32918.2-2016 §5.5; the draft's §5.1.4.4). This crate carries it +//! in the key traits' `ctx` parameter -- see [`sm2`]'s module docs for the full reasoning and the +//! resulting requirement that `ctx` must be `Some`. +//! +//! # Usage Examples +//! +//! Every SM2 operation needs the signer's identity `IDA`, which `ZA` binds into the message hash; +//! this crate carries it in `ctx`, and rejects `None`. Signing draws a fresh `k` from the RNG it +//! is given, so signatures are not reproducible: +//! +//! ``` +//! use bouncycastle_core::signature_key::{SigningKey, VerifyingKey}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! use bouncycastle_sm2::keys::keygen; +//! +//! let (pk, sk) = keygen(&mut HashDRBG_SHA512::new_from_os())?; +//! let mut rng = HashDRBG_SHA512::new_from_os(); +//! let id = b"alice@example.com"; +//! let message = b"the message to sign"; +//! +//! let signature = sk.sign(&mut rng, message, Some(id))?; +//! pk.verify(message, Some(id), &signature)?; +//! +//! // The identity is part of what is signed: verifying under a different one fails. +//! assert!(pk.verify(message, Some(b"bob@example.com"), &signature).is_err()); +//! // And `ctx` is not optional here, unlike every other primitive in this workspace. +//! assert!(sk.sign(&mut rng, message, None).is_err()); +//! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) +//! ``` +//! +//! Streaming, for a message too large to hold at once. `ZA` is absorbed, and `k` drawn, when the +//! signer is created, so only the message itself passes through `do_update`: +//! +//! ``` +//! use bouncycastle_core::signature_key::{SigningKey, VerifyingKey}; +//! use bouncycastle_core::traits::{SignatureSigner, SignatureVerifier}; +//! use bouncycastle_rng::HashDRBG_SHA512; +//! use bouncycastle_sm2::keys::keygen; +//! +//! let (pk, sk) = keygen(&mut HashDRBG_SHA512::new_from_os())?; +//! let id = b"alice@example.com"; +//! +//! let mut signer = sk.create_signer(&mut HashDRBG_SHA512::new_from_os(), Some(id))?; +//! signer.do_update(b"the first chunk, "); +//! signer.do_update(b"then the second"); +//! let signature = signer.do_final()?; +//! +//! let mut verifier = pk.create_verifier(Some(id))?; +//! verifier.do_update(b"the first chunk, then the second"); +//! verifier.do_verify_final(&signature)?; +//! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) +//! ``` +//! +//! Keys use the same SEC 1 encodings as `bouncycastle-ecdsa` -- `04 || X || Y` for a public key +//! (the 33-byte compressed form is also accepted on decode), a 32-byte big-endian integer for a +//! private key, with `dA` required to be in `[1, n-2]`: +//! +//! ``` +//! use bouncycastle_rng::HashDRBG_SHA512; +//! use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +//! use bouncycastle_sm2::keys::{SM2PrivateKey, SM2PublicKey, keygen}; +//! +//! let (pk, sk) = keygen(&mut HashDRBG_SHA512::new_from_os())?; +//! +//! assert_eq!(SM2PublicKey::from_bytes(&pk.encode())?, pk); +//! assert_eq!(SM2PrivateKey::from_bytes(&sk.encode())?, sk); +//! // A private key carries its own public key, so this costs nothing to ask for. +//! assert_eq!(sk.derive_pk(), pk); +//! +//! assert!(SM2PrivateKey::from_bytes(&[0u8; 32]).is_err()); +//! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) +//! ``` +//! +//! # Memory Footprint +//! +//! The following table lists the size of the on-disk bytes encoding and the in-memory struct size +//! of the key objects: +//! +//! | Key Object | PK size on disk | PK size in memory | SK size on disk | SK size in memory | +//! |------------|-----------------|--------------------|------------------|--------------------| +//! | SM2 | 65 | 64 | 32 | 128 | +//! +//! All values are in bytes. The "in memory" sizes are measured by rust's `std::mem::size_of`; the +//! "on disk" sizes are [`keys::PK_LEN`]/[`keys::SK_LEN`]. These numbers are produced by +//! `mem_usage_benches`' `bench_sm2_mem_usage` binary's `print_struct_sizes()`; that binary is also +//! the stack-usage measurement harness (see its own doc comment for the `valgrind`/`massif` +//! invocation). +//! +//! An `SM2PrivateKey` is 128 bytes in memory against a 32-byte encoding because it carries two +//! values derived from `dA` alongside it: the matching public key `PA` (two 32-byte field +//! elements) and `(1 + dA)^-1 mod n` (32 bytes, held in `Secret` like `dA` itself). Neither is +//! redundancy: `ZA` mixes `PA` into every signature, so without it each `sign` would have to +//! recompute `[dA]G` and cost two fixed-base scalar multiplications instead of one; and step A6 +//! divides by `1 + dA` on every signature, so without the cached inverse each `sign` paid a +//! constant-time inversion for a value that never changes. See [`keys::SM2PrivateKey`]'s own +//! docs. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! - **Everything done with the private key `dA` and the used per-message secret `k` is +//! branch-free and does not index memory by their value**: the inversion of `1 + dA`, +//! multiplication, the fixed-base scalar multiplication `[k]G`. `dA` lives in +//! [`bouncycastle_ec::sm2_scalar::Sm2Scalar`] (a [`bouncycastle_utils::secret::Secret`]). The +//! exceptions are GB/T 32918.2-2016 §6.1's two retry checks, step A5's `r = 0` or `r + k = n` +//! and step A6's `s = 0`: each is a branch, taken with probability about `2^-256`, and a taken +//! branch discards that `k` (the signer reports failure and the caller makes a fresh one), so +//! neither reveals anything about a `k` that is used. +//! - **`k` must never repeat across two signatures under the same key with different messages.** As +//! with ECDSA, a repeated `k` leaks `dA` directly from two signatures' `(r, s)` pairs. Unlike +//! `bouncycastle-ecdsa`'s curves, there is no deterministic (RFC 6979-style) default here: the +//! standard only specifies randomised `k` generation (GB/T 32918.2-2016 §6.1 step A3), so +//! [`SigningKey::create_signer`](bouncycastle_core::signature_key::SigningKey::create_signer) +//! draws `k` from the RNG it is handed, once per signer -- see [`sm2`]'s module docs. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +pub mod extra_bits; +pub mod keys; +pub mod sm2; +pub mod za; diff --git a/crypto/sm2/src/sm2.rs b/crypto/sm2/src/sm2.rs new file mode 100644 index 000000000..7b9742597 --- /dev/null +++ b/crypto/sm2/src/sm2.rs @@ -0,0 +1,389 @@ +//! `SM2`: GB/T 32918.2-2016 §6.1 (signature generation) and §7.1 (verification), the draft's §5.2 +//! and §5.3 -- the actual SM2 Digital Signature Algorithm, distinct from ECDSA despite both being +//! built on Weierstrass elliptic-curve arithmetic: SM2's signing equation `s = (1+dA)^-1 * (k - +//! r*dA) mod n` and its identity-binding `ZA` digest (see [`crate::za`]) have no ECDSA +//! equivalent. Fixed to SM3: GB/T 32918.2-2016 §5.4.2 (the draft's §5.1.4.2) admits any approved +//! hash "such as SM3", but SM3 is what the standard's own Appendix A example, the draft's, and +//! every deployed implementation use, so no other hash is offered. +//! +//! # `ctx` carries the signer's identity `IDA` +//! +//! Every operation here (`ZA` is prepended to the message in §6.1 step A1 / §7.1 step B3) needs +//! `IDA`, the signer's identity octet string (§5.5), to compute `ZA`. The key traits' `ctx` +//! parameter is the natural place for a per-signature binding value, so this crate treats +//! `ctx: Option<&[u8]>` as literally `IDA`, and [`SigningKey::create_signer`] / +//! [`VerifyingKey::create_verifier`] return [`SignatureError::GenericError`] if `ctx` is `None` -- +//! there is no default identity in the draft to fall back to, and inventing one unverified would +//! be worse than requiring the caller supply it explicitly. +//! +//! # Randomness, not determinism +//! +//! GB/T 32918.2-2016 §6.1 step A3 says only to produce `k` in `[1, n-1]` with a random number +//! generator (the draft's §5.2.1: "pick a random number `k`... using a random number +//! generator"); there is no SM2 analogue of RFC 6979 in either, so (unlike every curve in +//! `bouncycastle-ecdsa`) there is no [`DeterministicSigningKey`] impl here. `k` is drawn from the +//! RNG handed to [`SigningKey::create_signer`], once per signer, before the message is seen. +//! +//! # Retry +//! +//! Steps A5 (`r = 0` or `r + k = n`) and A6 (`s = 0`) each ask the signer to go back to step A3 and +//! draw a new `k`. The signer has no RNG at `do_final` to draw from, so it reports the failure +//! and the caller creates a fresh signer, as `bouncycastle-ecdsa`'s random-`k` signers do for +//! FIPS 186-5 §6.4.1 step 11; each case has probability about `2^-256`. +//! +//! # No DER encoding +//! +//! GB/T 32918.2-2016 has no ASN.1/DER encoding for `(r, s)`; [`SIG_LEN`] is raw `r || s`, 64 bytes, each a +//! 32-byte big-endian integer -- the same convention `bouncycastle-ecdsa` uses as its own default. +//! +//! # Extracting `x1` from a secret-derived `[k]G` +//! +//! `[k]G` (§6.1 step A4) is Jacobian-coordinate output of a *secret* scalar multiplication, so +//! converting it to affine cannot use [`Sm2JacobianPoint::to_affine`] -- that function's own docs +//! say it branches on infinity and is "not for use on a secret intermediate value". +//! [`x_affine_of_signing_point`] instead always computes `X * Z^-2`, branch-free; see its docs for +//! why `Z` is never zero here. This mirrors what every curve in `bouncycastle-ecdsa` does for the +//! same step, and is what makes this crate's "never handled in variable time" claim for `k` (see +//! [`crate`]'s Security Considerations) true of the signing path as a whole. + +use crate::extra_bits::reduce_wide_bits_mod_n_minus_1; +use crate::keys::{EXTRA_BITS_DRBG_OUTPUT_LEN, SM2PrivateKey, SM2PublicKey}; +use crate::za; +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::security_strength::SecurityStrength; +#[allow(unused_imports)] // for the docs +use bouncycastle_core::signature_key::DeterministicSigningKey; +use bouncycastle_core::signature_key::{SignatureAlgorithm, SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{ + Algorithm, AlgorithmInfo, Hash, RNG, SignatureSigner, SignatureVerifier, +}; +use bouncycastle_ec::nat; +use bouncycastle_ec::sm2::Sm2FieldElement; +use bouncycastle_ec::sm2_comb::comb_multiply_base_point; +use bouncycastle_ec::sm2_point::Sm2JacobianPoint; +use bouncycastle_ec::sm2_scalar::{N_LIMBS, Sm2PublicScalar, Sm2Scalar, Sm2ScalarField}; +use bouncycastle_ec::sm2_sec1; +use bouncycastle_ec::sm2_wnaf::shamir_multiply; +use bouncycastle_sm3::SM3; +use bouncycastle_utils::secret::Secret; + +/// Raw `r || s` signature length: two 32-byte field-width integers. See the module docs on why +/// there is no DER alternative here. +pub const SIG_LEN: usize = 64; + +/// SM2 digital signature (GB/T 32918.2-2016): the algorithm marker. [`SM2PrivateKey`] signs +/// through [`SigningKey`], [`SM2PublicKey`] verifies through [`VerifyingKey`]. `ctx` must carry +/// the signer's identity `IDA` (S5.5): `ZA` binds it into every signature, so a missing `ctx` is +/// an error rather than a default identity. +pub struct SM2; + +impl Algorithm for SM2 { + const ALG_NAME: &'static str = "SM2"; + /// SP 800-57 Part 1 Rev. 5, Table 2: a 256-bit curve order gives 128 bits. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl SignatureAlgorithm for SM2 {} + +/// The signer [`SigningKey::create_signer`] hands out. `ZA` is absorbed when it is created, and +/// the per-message secret `k` (step A3) is drawn from the caller's RNG then too, since it does +/// not depend on the message; the signer then only hashes. +pub struct SM2Signer<'k> { + sk: &'k SM2PrivateKey, + hash: SM3, + k: Sm2Scalar, +} + +/// The verifier [`VerifyingKey::create_verifier`] hands out; `ZA` is absorbed when it is created. +pub struct SM2Verifier<'k> { + pk: &'k SM2PublicKey, + hash: SM3, +} + +/// `e` from the (already `ZA`-prefixed) running hash, per §6.1 step A2 / §7.1 step B4. +fn e_from_hash(hash: SM3) -> Sm2ScalarField { + let mut e_bytes = [0u8; 32]; + hash.do_final_out(&mut e_bytes); + Sm2ScalarField::from_limbs(sm2_sec1::limbs_from_be_bytes(&e_bytes)) +} + +/// `x * z^-2`, branch-free -- the affine `x1` of a point that came out of a *secret* scalar +/// multiplication, mirroring `bouncycastle_ecdsa::ecdsa_p256`'s helper of the same name. +/// +/// [`Sm2JacobianPoint::to_affine`] cannot be used here: its own docs say it branches on infinity +/// and is "not for use on a secret intermediate value", and `[k]G` is exactly that. This always +/// computes `X * Z^-2` instead, which is what `to_affine` does in its non-infinity case. `Z` is +/// never `0` here, since `k` is in `[1, n-1]` by construction and `G` has prime order `n` (SM2's +/// cofactor is `h = 1`), so `[k]G` is never the identity for any valid `k` -- that holds +/// unconditionally, so skipping the infinity branch leaks nothing about which `k` was drawn. It +/// also skips computing `y1`, which step A5 never uses. +fn x_affine_of_signing_point(point: &Sm2JacobianPoint) -> Sm2FieldElement { + let z_inv = point.z.invert(); + point.x.mul(&z_inv.mul(&z_inv)) +} + +/// §6.1 steps A4-A7, given `k` already generated (step A3) by one of `SM2`'s randomised methods. +/// Returns `None` for step A5's `r = 0`/`r + k = n` or step A6's `s = 0`, on which +/// [`sign_with_random_k`] goes back to step A3. +fn sign_with_k(sk: &SM2PrivateKey, e: &Sm2ScalarField, k: Sm2Scalar) -> Option<[u8; SIG_LEN]> { + let mut k_field = Sm2ScalarField::from_secret(&k); + + let r_point = comb_multiply_base_point(&k); // step A4 + let x1 = x_affine_of_signing_point(&r_point); // step A4's x1 + let x1_field = Sm2ScalarField::from_limbs(x1.to_limbs()); + let r = e.add(&x1_field); // step A5 + + // `r + k` is as sensitive as `k`: `r` is published in the signature, so anyone holding both + // recovers `k` by subtraction. It is scrubbed on both ways out of the check below. + let mut r_plus_k = r.add(&k_field); + // Step A5's check. Both disjuncts are compared in constant time (`PartialEq` on the field + // type is), and the branch below is one of the two data-dependent branches on the signing + // path (step A6's `s = 0` is the other). `r = 0` is a condition on the public output; + // `(r + k) mod n = 0`, i.e. `r + k = n` for `k` in `[1, n-1]` and `r` in `[0, n-1]`, is a + // function of `k`, but a taken branch discards that `k`, so it reveals nothing about a `k` + // that is used. Each disjunct is taken with probability about `2^-256` per signature. + let r_out_of_range = r_is_zero_or_r_plus_k_is_zero(&r, &r_plus_k); + r_plus_k.zeroize(); + if r_out_of_range { + k_field.zeroize(); + return None; + } + + let mut d_field = Sm2ScalarField::from_secret(sk.scalar()); + // (1 + dA)^-1 is cached on the key (see SM2PrivateKey's docs): it depends only on dA, and + // recomputing it here was a constant-time inversion per signature. + let mut one_plus_d_inv = Sm2ScalarField::from_secret(sk.one_plus_d_inv()); + let s = one_plus_d_inv.mul(&k_field.sub(&r.mul(&d_field))); // step A6 + + // `k_field`, `d_field` and `(1 + dA)^-1` all reveal `k` or `dA`; nothing below needs them, and + // scrubbing here covers both the error return and the success return. `k` itself is a `Secret` + // and scrubs when it drops; `e`, `r` and `s` are public by construction. + k_field.zeroize(); + d_field.zeroize(); + one_plus_d_inv.zeroize(); + + if s == Sm2ScalarField::ZERO { + return None; + } + + let mut sig = [0u8; SIG_LEN]; + sig[..32].copy_from_slice(&sm2_sec1::be_bytes_from_limbs(&r.to_limbs())); + sig[32..].copy_from_slice(&sm2_sec1::be_bytes_from_limbs(&s.to_limbs())); + Some(sig) // step A7 +} + +/// Step A3: `k` from the caller's RNG, by the extra-random-bits reduction of +/// [`reduce_wide_bits_mod_n_minus_1`]. +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + +/// §6.1 step A5's `r = 0` or `r + k = n` check, named so it has something to unit test directly: +/// reaching either branch through `sign_with_k` needs an `r`/`r + k` value that is astronomically +/// unlikely for any real `k`/`e` (see `sign_with_k`'s call site). +fn r_is_zero_or_r_plus_k_is_zero(r: &Sm2ScalarField, r_plus_k: &Sm2ScalarField) -> bool { + *r == Sm2ScalarField::ZERO || *r_plus_k == Sm2ScalarField::ZERO +} + +/// `SM3` with `ZA = H256(ENTL || IDA || a || b || xG || yG || xA || yA)` absorbed (S5.5), the +/// state both the signer and the verifier start from. `ctx` is `IDA`. +fn za_hash( + ctx: Option<&[u8]>, + x: &Sm2FieldElement, + y: &Sm2FieldElement, +) -> Result { + let id = ctx.ok_or(SignatureError::GenericError( + "SM2 requires ctx to carry the signer's identity IDA (GB/T 32918.2-2016 S5.5); see \ + bouncycastle-sm2's crate docs", + ))?; + let za = za::compute(id, x, y)?; + let mut hash = SM3::new(); + hash.do_update(&za); + Ok(hash) +} + +impl SigningKey for SM2PrivateKey { + type Signer<'k> + = SM2Signer<'k> + where + Self: 'k; + + fn create_signer<'k>( + &'k self, + rng: &mut dyn RNG, + ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + // PA is carried by the key, not recomputed: see SM2PrivateKey's docs. + let (x, y) = self.derive_pk().affine(); + Ok(SM2Signer { sk: self, hash: za_hash(ctx, &x, &y)?, k: random_k(rng)? }) + } +} + +impl VerifyingKey for SM2PublicKey { + type Verifier<'k> + = SM2Verifier<'k> + where + Self: 'k; + + fn create_verifier<'k>( + &'k self, + ctx: Option<&[u8]>, + ) -> Result, SignatureError> { + let (x, y) = self.affine(); + Ok(SM2Verifier { pk: self, hash: za_hash(ctx, &x, &y)? }) + } +} + +impl AlgorithmInfo for SM2Signer<'_> { + fn alg_name(&self) -> &'static str { + SM2::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + SM2::MAX_SECURITY_STRENGTH + } +} + +impl SignatureSigner for SM2Signer<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + let e = e_from_hash(self.hash); + // Steps A5 and A6 send `r = 0`, `r + k = n` and `s = 0` back to step A3 for a new `k`. + // `k` was drawn when this signer was created and there is no RNG here to draw another + // from, so the failure is reported instead; each case has probability about 2^-256. + sign_with_k(self.sk, &e, self.k).ok_or(SignatureError::GenericError( + "SM2 produced r = 0, r + k = n or s = 0; create a fresh signer and sign again", + )) + } + + fn do_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_final()?; + Ok(SIG_LEN) + } +} + +impl AlgorithmInfo for SM2Verifier<'_> { + fn alg_name(&self) -> &'static str { + SM2::ALG_NAME + } + + fn max_security_strength(&self) -> SecurityStrength { + SM2::MAX_SECURITY_STRENGTH + } +} + +impl SignatureVerifier for SM2Verifier<'_> { + fn do_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + verify_final(self.hash, self.pk, sig) + } +} + +/// Steps B1-B7 of the verification, given the `ZA`-primed hash with the message absorbed. +fn verify_final(hash: SM3, pk: &SM2PublicKey, sig: &[u8]) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::LengthError("SM2 signature must be exactly 64 bytes")); + } + + // steps B1-B2: r', s' in [1, n-1], rejected (not reduced) if out of range. + let r_limbs = sm2_sec1::limbs_from_be_bytes(&sig[..32].try_into().unwrap()); + let s_limbs = sm2_sec1::limbs_from_be_bytes(&sig[32..64].try_into().unwrap()); + if either_out_of_range(&r_limbs, &s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = Sm2ScalarField::from_limbs(r_limbs); + let s = Sm2ScalarField::from_limbs(s_limbs); + + let e = e_from_hash(hash); // steps B3-B4 + + let t = r.add(&s); // step B5 + if t == Sm2ScalarField::ZERO { + return Err(SignatureError::SignatureVerificationFailed); + } + + let q = Sm2JacobianPoint::from_affine(pk.x, pk.y); + let s_pub = Sm2PublicScalar::from_limbs(s.to_limbs()); + let t_pub = Sm2PublicScalar::from_limbs(t.to_limbs()); + let point = shamir_multiply(&s_pub, &t_pub, &q); // step B6 + + if point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x1, _) = point.to_affine().expect("just checked point is not the point at infinity"); + let r1 = e.add(&Sm2ScalarField::from_limbs(x1.to_limbs())); // step B7's R + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step B7 +} + +fn in_range(limbs: &[u64; 4]) -> bool { + if *limbs == [0, 0, 0, 0] { + return false; + } + let (_, borrow) = nat::sub(limbs, &N_LIMBS); + borrow == 1 +} + +/// `true` iff either `r'` or `s'` is outside `[1, n-1]` -- §7.1 steps B1-B2's range check, named so +/// the OR-of-two-checks combination has something to unit test directly (mirroring +/// `bouncycastle_ec::sm2_sec1::either_out_of_range`'s own naming and reasoning). +fn either_out_of_range(r_limbs: &[u64; 4], s_limbs: &[u64; 4]) -> bool { + !in_range(r_limbs) || !in_range(s_limbs) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn in_range_boundary_cases() { + assert!(!in_range(&[0, 0, 0, 0])); + assert!(in_range(&[1, 0, 0, 0])); + let n_minus_1 = [N_LIMBS[0] - 1, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]]; + assert!(in_range(&n_minus_1)); + assert!(!in_range(&N_LIMBS)); + assert!(!in_range(&[u64::MAX; 4])); + } + + // r_is_zero_or_r_plus_k_is_zero is private and, through sign_with_k, only ever reached with an + // r/r+k value that is astronomically unlikely for any real k/e (see its own doc comment), so no + // realistic integration test exercises either disjunct -- the QUALITY_AND_STYLE.md + // private-function exception applies (the same one bouncycastle-ecdsa's own + // r_or_s_is_zero_requires_only_one_to_be_zero test relies on). + #[test] + fn r_is_zero_or_r_plus_k_is_zero_requires_only_one_to_be_zero() { + let zero = Sm2ScalarField::ZERO; + let one = Sm2ScalarField::ONE; + assert!(!r_is_zero_or_r_plus_k_is_zero(&one, &one)); + assert!(r_is_zero_or_r_plus_k_is_zero(&zero, &one)); + assert!(r_is_zero_or_r_plus_k_is_zero(&one, &zero)); + assert!(r_is_zero_or_r_plus_k_is_zero(&zero, &zero)); + } + + #[test] + fn either_out_of_range_requires_only_one_to_be_out_of_range() { + let in_range = [1, 0, 0, 0]; + let out_of_range = N_LIMBS; + assert!(!either_out_of_range(&in_range, &in_range)); + assert!(either_out_of_range(&out_of_range, &in_range)); + assert!(either_out_of_range(&in_range, &out_of_range)); + assert!(either_out_of_range(&out_of_range, &out_of_range)); + } +} diff --git a/crypto/sm2/src/za.rs b/crypto/sm2/src/za.rs new file mode 100644 index 000000000..a9931a9b6 --- /dev/null +++ b/crypto/sm2/src/za.rs @@ -0,0 +1,65 @@ +//! `ZA` computation: GB/T 32918.2-2016 §5.5 (the draft's §5.1.4.4), `ZA = SM3(ENTLA || IDA || a || +//! b || xG || yG || xA || yA)` -- the identity-and-domain-parameter digest that binds an SM2 +//! signature to a specific signer's identity `IDA` and public key `(xA, yA)`, distinct from §6.1 +//! steps A1-A2's `e = SM3(ZA || M)` hash of the actual message. `ENTLA` is `IDA`'s bit length as a +//! 2-byte big-endian integer; `a`, +//! `b`, `xG`, `yG`, `xA`, `yA` are each encoded as fixed-width 32-byte big-endian octets, matching +//! this curve's field size. +//! +//! Verified (in Python, not checked in) against the draft's own Appendix A.2 worked example before +//! any of this was written: `IDA = "ALICE123@YAHOO.COM"` (18 ASCII bytes) gives `ENTLA = 0x0090` +//! (144 bits), and every intermediate byte string this module builds -- `ENTLA || IDA`, then each +//! domain/key coordinate appended -- was checked against the draft's own printed values, as was the +//! resulting `ZA`. That worked example uses a different (illustrative) curve than the "recommended +//! parameters" curve `bouncycastle_ec::sm2` implements, so this only validates the algorithm and +//! byte-encoding, not this crate's own domain constants -- those are separately validated in +//! `bouncycastle-ec`'s own SM2 tests. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::traits::Hash; +use bouncycastle_ec::nat; +use bouncycastle_ec::sm2::{P_LIMBS, Sm2FieldElement}; +use bouncycastle_ec::sm2_domain::{B_LIMBS, G_X_LIMBS, G_Y_LIMBS}; +use bouncycastle_ec::sm2_sec1::be_bytes_from_limbs; +use bouncycastle_sm3::SM3; + +/// `a = p - 3` (`draft-shen-sm2-ecdsa-02` Appendix D), computed here rather than exposed by +/// [`bouncycastle_ec::sm2_domain`]: SM2's own curve arithmetic never needs the literal value of `a` +/// (it bakes the `a = -3` shortcut directly into its doubling formula -- see +/// `bouncycastle_ec::sm2_point`'s docs), but `ZA`'s definition below does. +fn a_limbs() -> [u64; 4] { + let (diff, borrow) = nat::sub(&P_LIMBS, &[3, 0, 0, 0]); + debug_assert_eq!(borrow, 0, "p > 3, so p - 3 never borrows"); + diff +} + +/// Computes `ZA` for the given identity `id` and public key affine coordinates +/// (GB/T 32918.2-2016 §5.5). Returns [`SignatureError::GenericError`] if `id`'s bit length +/// doesn't fit in 16 bits, the limit `ENTLA`'s 2-byte encoding imposes. +pub fn compute( + id: &[u8], + x: &Sm2FieldElement, + y: &Sm2FieldElement, +) -> Result<[u8; 32], SignatureError> { + let bit_len = id + .len() + .checked_mul(8) + .ok_or(SignatureError::GenericError("SM2 identity too long: bit length overflows usize"))?; + let entla: u16 = bit_len.try_into().map_err(|_| { + SignatureError::GenericError("SM2 identity too long: ENTLA must fit in 16 bits") + })?; + + let mut hash = SM3::new(); + hash.do_update(&entla.to_be_bytes()); + hash.do_update(id); + hash.do_update(&be_bytes_from_limbs(&a_limbs())); + hash.do_update(&be_bytes_from_limbs(&B_LIMBS)); + hash.do_update(&be_bytes_from_limbs(&G_X_LIMBS)); + hash.do_update(&be_bytes_from_limbs(&G_Y_LIMBS)); + hash.do_update(&be_bytes_from_limbs(&x.to_limbs())); + hash.do_update(&be_bytes_from_limbs(&y.to_limbs())); + + let mut za = [0u8; 32]; + hash.do_final_out(&mut za); + Ok(za) +} diff --git a/crypto/sm2/tests/extra_bits_tests.rs b/crypto/sm2/tests/extra_bits_tests.rs new file mode 100644 index 000000000..f429443f6 --- /dev/null +++ b/crypto/sm2/tests/extra_bits_tests.rs @@ -0,0 +1,209 @@ +//! Known-answer tests for [`reduce_wide_bits_mod_n_minus_1`] (FIPS 186-5 Appendix A.4.1 steps +//! 3-5): `x = bits2int(X)`, `x = x mod (n-1)`, `x = x + 1`, and for +//! [`reduce_wide_bits_mod_n_minus_2`], the same steps with modulus `n - 2`. Expected values +//! computed independently in Python via `(int.from_bytes(bytes, 'big') % m) + 1` with `n` from +//! `draft-shen-sm2-ecdsa-02` Appendix D, not derived from this crate's own reduction code -- +//! existing round-trip tests elsewhere (`keygen`/`sign_randomized`) only ever check that the +//! reduced value is self-consistent with itself, which can't distinguish a correct reduction from +//! a systematically wrong one. Mirrors `bouncycastle-ecdsa`'s own `extra_bits_bp256r1_tests.rs`, +//! written for the identical reason there: a `cargo mutants` run on this function and its +//! `N_MINUS_1_LIMBS`/`TWO_POW_256_MOD_N_MINUS_1_LIMBS` constants surfaced several bit-level and +//! off-by-one mutants surviving because nothing pinned them against an independent reference. + +use bouncycastle_ec::nat; +use bouncycastle_ec::sm2_scalar::N_LIMBS; +use bouncycastle_ec::sm2_sec1::be_bytes_from_limbs; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_sm2::extra_bits::{ + reduce_wide_bits_mod_n_minus_1, reduce_wide_bits_mod_n_minus_2, +}; + +fn bytes32(hex: &str) -> [u8; 32] { + hex_decode(hex).unwrap().try_into().unwrap() +} + +/// `n - 1` and `n - 2` as limbs: `n` is odd and `N_LIMBS[0]` ends `0x23`, so neither borrows. +fn n_minus_1() -> [u64; 4] { + [N_LIMBS[0] - 1, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]] +} + +fn n_minus_2() -> [u64; 4] { + [N_LIMBS[0] - 2, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]] +} + +/// `value`, big-endian, left-padded to the 40-byte DRBG output width this module is fed with. +fn padded_to_40(value: &[u64; 4]) -> [u8; 40] { + let mut input = [0u8; 40]; + input[8..].copy_from_slice(&be_bytes_from_limbs(value)); + input +} + +#[test] +fn known_answer_repeating_pattern() { + let input: Vec = [0x01u8, 0x02, 0x03, 0x04].iter().cloned().cycle().take(40).collect(); + let expected = bytes32("0306090c0102030401911cc9047a6deaf6e477a96f45da1d0d752effea492df5"); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_ff() { + let input = [0xffu8; 40]; + let expected = bytes32("00000001000000008dfc20956c361b6a187a276050a8c5a9726ecad4c62abede"); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_zero_yields_one() { + // bits2int(0) = 0; 0 mod (n-1) = 0; +1 = 1 -- the smallest value Appendix A.4.1 can produce. + let input = [0u8; 40]; + let mut expected = [0u8; 32]; + expected[31] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), expected); +} + +/// The two boundaries of Appendix A.4.1's own arithmetic: `n - 1` is the modulus of step 4, so it +/// reduces to `0` and step 5 makes it `1`; `n - 2` is the largest residue, so it comes out as +/// `n - 1`, the top of the `[1, n-1]` output interval. +#[test] +fn known_answer_at_the_n_minus_1_boundary() { + let mut one = [0u8; 32]; + one[31] = 1; + let input = padded_to_40(&n_minus_1()); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), one, "n - 1 -> 1"); + + let input = padded_to_40(&n_minus_2()); + let n_minus_1 = be_bytes_from_limbs(&n_minus_1()); + assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), n_minus_1, "n - 2 -> n - 1"); +} + +#[test] +fn known_answer_repeating_pattern_mod_n_minus_2() { + let input: Vec = [0x01u8, 0x02, 0x03, 0x04].iter().cloned().cycle().take(40).collect(); + let expected = bytes32("0306090c0102030401911cc9047a6deaf6e477a96f45da1d0e773203ec4d33fd"); + assert_eq!(reduce_wide_bits_mod_n_minus_2(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_ff_mod_n_minus_2() { + let input = [0xffu8; 40]; + let expected = bytes32("00000001000000008dfc20956c361b6a187a276050a8c5aa726ecad5c62abedf"); + assert_eq!(reduce_wide_bits_mod_n_minus_2(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_zero_yields_one_mod_n_minus_2() { + let input = [0u8; 40]; + let mut expected = [0u8; 32]; + expected[31] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_2(&input).to_be_bytes(), expected); +} + +/// The same boundaries one lower: `n - 2` is the modulus, so it reduces to `0` and comes out `1`; +/// `n - 3` is the largest residue and comes out as `n - 2`, the top of the `[1, n-2]` output +/// interval; and `n - 1`, the value the `[1, n-1]` range used to admit as a private key, is `1` +/// above the modulus and comes out `2`. +#[test] +fn known_answer_at_the_n_minus_2_boundary() { + let mut one = [0u8; 32]; + one[31] = 1; + let mut two = [0u8; 32]; + two[31] = 2; + let n_minus_3 = [N_LIMBS[0] - 3, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]]; + + let input = padded_to_40(&n_minus_2()); + assert_eq!(reduce_wide_bits_mod_n_minus_2(&input).to_be_bytes(), one, "n - 2 -> 1"); + + let input = padded_to_40(&n_minus_3); + let n_minus_2 = be_bytes_from_limbs(&n_minus_2()); + assert_eq!(reduce_wide_bits_mod_n_minus_2(&input).to_be_bytes(), n_minus_2, "n - 3 -> n - 2"); + + let input = padded_to_40(&n_minus_1()); + assert_eq!(reduce_wide_bits_mod_n_minus_2(&input).to_be_bytes(), two, "n - 1 -> 2"); +} + +/// The bit-serial algorithm this module implemented before switching to Barrett reduction, kept +/// here as an independent reference: Horner's rule over the input bits, doubling the running +/// remainder and subtracting the modulus `m` when it is exceeded, with the carry out of the top +/// limb folded back in as `2^256 mod m = 2^256 - m`. Slow, obviously correct, and sharing no code +/// with the Barrett path beyond `nat`'s add and sub. +fn reference_bit_serial(bytes: &[u8], m: &[u64; 4]) -> [u8; 32] { + let two_pow_mod_m = nat::sub(&[0u64; 4], m).0; // 2^256 - m, wrapping + let mut acc = [0u64; 4]; + for &byte in bytes { + for bit_idx in (0..8).rev() { + let (mut doubled, carry) = nat::add(&acc, &acc); + doubled[0] |= ((byte >> bit_idx) & 1) as u64; + if carry == 1 { + doubled = nat::add(&doubled, &two_pow_mod_m).0; + } + let (reduced, borrow) = nat::sub(&doubled, m); + acc = if borrow == 1 { doubled } else { reduced }; + } + } + let plus_one = nat::add(&acc, &[1u64, 0, 0, 0]).0; + be_bytes_from_limbs(&plus_one) +} + +/// xorshift64*, fixed seed, as in the `ec` crate's property tests. +struct Xorshift64(u64); + +impl Xorshift64 { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } +} + +/// Barrett against the bit-serial reference over pseudorandom inputs at every width from one byte +/// up to the 63-byte maximum the functions accept, biased towards the widths actually drawn +/// (40 bytes) and towards values with long runs of set bits, where Barrett's quotient estimate is +/// furthest off and the most conditional subtractions are needed. Both moduli, on the same inputs. +#[test] +fn agrees_with_the_bit_serial_reference_over_many_pseudorandom_inputs() { + let mut rng = Xorshift64(0xBA77E770_0000_0004); + for i in 0..3000 { + let len = match i % 4 { + 0 => 40, + 1 => 63, + _ => 1 + (rng.next_u64() as usize % 63), + }; + let mut bytes = vec![0u8; len]; + for chunk in bytes.chunks_mut(8) { + let word = rng.next_u64().to_be_bytes(); + chunk.copy_from_slice(&word[..chunk.len()]); + } + if i % 3 == 0 { + for b in bytes.iter_mut().take(len / 2) { + *b = 0xff; + } + } + assert_eq!( + reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), + reference_bit_serial(&bytes, &n_minus_1()), + "mod n - 1: len = {len}, bytes = {bytes:02x?}" + ); + assert_eq!( + reduce_wide_bits_mod_n_minus_2(&bytes).to_be_bytes(), + reference_bit_serial(&bytes, &n_minus_2()), + "mod n - 2: len = {len}, bytes = {bytes:02x?}" + ); + } +} + +/// `2^504 - 1`, the largest input accepted: Barrett's precondition holds with room to spare. The +/// `n - 2` answer is pinned from Python as well as against the reference. +#[test] +fn maximum_width_all_ones_input() { + let bytes = [0xffu8; 63]; + assert_eq!( + reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), + reference_bit_serial(&bytes, &n_minus_1()) + ); + let expected = bytes32("a21eb5e374a22b3d3d620fc84e3affe07e92c8346dee9756110ae2df3783fc97"); + assert_eq!(reduce_wide_bits_mod_n_minus_2(&bytes).to_be_bytes(), expected); + assert_eq!(expected, reference_bit_serial(&bytes, &n_minus_2())); +} diff --git a/crypto/sm2/tests/sm2_kat_tests.rs b/crypto/sm2/tests/sm2_kat_tests.rs new file mode 100644 index 000000000..f8f457844 --- /dev/null +++ b/crypto/sm2/tests/sm2_kat_tests.rs @@ -0,0 +1,195 @@ +//! Known-answer tests for [`SM2::verify`] against `draft-shen-sm2-ecdsa-02` Appendix D's actual +//! "recommended parameters" curve (the curve `bouncycastle-ec`'s `sm2` module implements) -- unlike +//! this crate's [`crate::za`]-validation notes, which cross-check against the draft's own Appendix +//! A.2 worked example (a *different*, illustrative curve), these vectors were independently +//! generated in Python from scratch, against the real production curve, using: a from-scratch +//! affine-coordinate implementation of the group law (not ported from any Rust code here), Python's +//! `hashlib.new("sm3", ...)` (OpenSSL-backed) for the hash, and this crate's already-verified `ZA` +//! and signing-equation logic (steps A1-A7/B1-B7, matched against the draft's Appendix A.2 example +//! before any Rust was written). `d`, `k` below are arbitrary fixed test-only integers, not real +//! keys. +//! +//! These tests exercise [`SM2::verify`] (not `sign`): this crate's `k` is drawn through +//! [`bouncycastle_ec`]'s wide-DRBG-output reduction ([`crate::extra_bits`]), whose output for a +//! given raw DRBG string does not equal the raw `k` these Python vectors used directly, so signing +//! with a matching `k` isn't reproducible through the public API. Verification has no such +//! constraint: it only needs a public key and a signature, both computed once in Python and fixed +//! here. +//! +//! `bc_java_sm2p256v1_vector_verifies` below is the exception, and the only vector here whose +//! expected values did not originate with this workspace at all: it is BouncyCastle Java's own +//! `SM2SignerTest.doSignerTestFpStandardSM3` case (`core/src/test/java/org/bouncycastle/crypto/ +//! test/SM2SignerTest.java`), on the same `sm2p256v1` curve with SM3. A separate implementation +//! produced it, so it cross-checks this crate's domain constants, `ZA`, `e = SM3(ZA || M)` and the +//! GB/T 32918.2-2016 §7.1 verification equation against something external -- which the +//! Python-generated vectors +//! above, sharing this crate's own `ZA` and signing logic, cannot do. It was additionally +//! reproduced from scratch (in Python, not checked in: affine group law, `hashlib.new("sm3")`, +//! and the draft's published domain parameters) before being written here, so it rests on two +//! independent derivations rather than on trusting either one. + +use bouncycastle_core::signature_key::VerifyingKey; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_ec::sm2_sec1; +use bouncycastle_sm2::keys::{SM2PrivateKey, SM2PublicKey}; + +fn pubkey_from_hex(x_hex: &str, y_hex: &str) -> SM2PublicKey { + let mut bytes = [0u8; 65]; + bytes[0] = 0x04; + bytes[1..33].copy_from_slice(&bouncycastle_hex::decode(x_hex).unwrap()); + bytes[33..65].copy_from_slice(&bouncycastle_hex::decode(y_hex).unwrap()); + SM2PublicKey::from_bytes(&bytes).unwrap() +} + +fn sig_from_hex(r_hex: &str, s_hex: &str) -> [u8; 64] { + let mut sig = [0u8; 64]; + sig[..32].copy_from_slice(&bouncycastle_hex::decode(r_hex).unwrap()); + sig[32..].copy_from_slice(&bouncycastle_hex::decode(s_hex).unwrap()); + sig +} + +struct Vector { + qx: &'static str, + qy: &'static str, + id: &'static [u8], + msg_hex: &'static str, + r: &'static str, + s: &'static str, +} + +const V1: Vector = Vector { + qx: "17b937e7b5d2d00ceaec4ff25c6b74eaa57c8b67518fb630ec28ac04faf97f24", + qy: "71fa5c9eb79214b79568b2597a3c87fe8d3c0fb9a96b3cc36594bff402218fb1", + id: b"ALICE123@YAHOO.COM", + msg_hex: "6d65737361676520646967657374", + r: "250273e780744ebc1578119e0d1355e16ed1a75233ccae4c897fdab262908b05", + s: "1203d8c1b8341f5a668ff87979ba89cae12300a170849a2d47cf9d57a9d19362", +}; + +const V2_EMPTY_ID_AND_MSG: Vector = Vector { + qx: "32c4ae2c1f1981195f9904466a39c9948fe30bbff2660be1715a4589334c74c7", + qy: "bc3736a2f4f6779c59bdcee36b692153d0a9877cc62a474002df32e52139f0a0", + id: b"", + msg_hex: "", + r: "12c8bbc33bb8ebed2883e559dd4a26a4720659b025dc8c1414a3d6c00305b9e7", + s: "769ba21de2238a096bbe0d53115aecad7ffec2dd7df4bc8b9f8c0ea49b67c39f", +}; + +const V3_LONGER_MESSAGE: Vector = Vector { + qx: "6d06baebf91d46e720c2a37f59ae6a070efdc56aa79be2c992e9bed2af4706bf", + qy: "8a8c2b9d86f05a6fb94ba694cf0ea5bcaf48020fcd22cdef90c118b522b3a9d1", + id: b"user@example.com", + msg_hex: "54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f6754686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f6754686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67", + r: "f8824e311760a45e6ea155514ec7a09260058657911c4ea8d86febfe002f5d9c", + s: "9016d56b07b2acb2677869e049a4d99cc33e0352d9cbf8f6dd86bead038c29fa", +}; + +const VECTORS: [Vector; 3] = [V1, V2_EMPTY_ID_AND_MSG, V3_LONGER_MESSAGE]; + +#[test] +fn known_answer_vectors_verify() { + for v in VECTORS { + let pk = pubkey_from_hex(v.qx, v.qy); + let msg = bouncycastle_hex::decode(v.msg_hex).unwrap(); + let sig = sig_from_hex(v.r, v.s); + pk.verify(&msg, Some(v.id), &sig) + .unwrap_or_else(|e| panic!("KAT vector failed to verify: {e:?}")); + } +} + +#[test] +fn known_answer_vectors_reject_tampered_r() { + for v in VECTORS { + let pk = pubkey_from_hex(v.qx, v.qy); + let msg = bouncycastle_hex::decode(v.msg_hex).unwrap(); + let mut sig = sig_from_hex(v.r, v.s); + sig[0] ^= 0xFF; + assert!(pk.verify(&msg, Some(v.id), &sig).is_err()); + } +} + +#[test] +fn known_answer_vectors_reject_wrong_id() { + for v in VECTORS { + let pk = pubkey_from_hex(v.qx, v.qy); + let msg = bouncycastle_hex::decode(v.msg_hex).unwrap(); + let sig = sig_from_hex(v.r, v.s); + assert!(pk.verify(&msg, Some(b"wrong identity"), &sig).is_err()); + } +} + +#[test] +fn known_answer_za_matches_independent_python_computation() { + // Cross-checks crate::za::compute directly, not just the end-to-end verify result -- pins the + // intermediate ZA value itself (GB/T 32918.2-2016 S5.5), independent of e/r/s. + let expected: [(&str, &str, &[u8], &str); 3] = [ + (V1.qx, V1.qy, V1.id, "bab69c9204aadd3d49ff244e8fdffb204e1d4249e36855f06a912abd81ddf167"), + ( + V2_EMPTY_ID_AND_MSG.qx, + V2_EMPTY_ID_AND_MSG.qy, + V2_EMPTY_ID_AND_MSG.id, + "c13adcc1829f563f2a01ef3c4e0685647bf32a650a35273443150d44f5809ff8", + ), + ( + V3_LONGER_MESSAGE.qx, + V3_LONGER_MESSAGE.qy, + V3_LONGER_MESSAGE.id, + "65052e995e872be0fb904d14ba6c2550a70d751e4dccf3c7a700c4588852e215", + ), + ]; + for (qx, qy, id, za_hex) in expected { + let x_limbs = sm2_sec1::limbs_from_be_bytes( + &bouncycastle_hex::decode(qx).unwrap().try_into().unwrap(), + ); + let y_limbs = sm2_sec1::limbs_from_be_bytes( + &bouncycastle_hex::decode(qy).unwrap().try_into().unwrap(), + ); + let x = bouncycastle_ec::sm2::Sm2FieldElement::from_limbs(x_limbs); + let y = bouncycastle_ec::sm2::Sm2FieldElement::from_limbs(y_limbs); + let za = bouncycastle_sm2::za::compute(id, &x, &y).unwrap(); + assert_eq!(bouncycastle_hex::encode(za), za_hex); + } +} + +/// BouncyCastle Java's `SM2SignerTest.doSignerTestFpStandardSM3` vector -- see this file's module +/// docs for its provenance and why it is the one externally-sourced case here. +/// +/// Unlike the vectors above it comes with its private key, so this also pins +/// [`SM2PrivateKey::derive_pk`] (`PA = [dA]G`) and [`bouncycastle_sm2::za::compute`] against the +/// same external source, not just the end-to-end verify result. +#[test] +fn bc_java_sm2p256v1_vector_verifies() { + const D_HEX: &str = "110e7973206f68c19ee5f7328c036f26911c8c73b4e4f36ae3291097f8984ffc"; + const PA_X: &str = "d03d30dd01ca3422aeaccf9b88043b554659d3092b0a9e8cce3e8c4530a98cb7"; + const PA_Y: &str = "9d705e6213eee145b748e36e274e5f101dc10d7bbc9dab9a04022e73b76e02cd"; + const ZA_HEX: &str = "ad1c1335ec4bd951cbb4a2144b8247b78fbe47efce2203086f968be8b730393d"; + const ID: &[u8] = b"sm2test@example.com"; + const MSG: &[u8] = b"hi chappy"; + const R: &str = "05890b9077b92e47b17a1ff42a814280e556afd92b4a98b9670bf8b1a274c2fa"; + const S: &str = "e3abbb8db2b6ecd9b24eccea7f679fb9a4b1db52f4aa985e443ad73237fa1993"; + + let sk = SM2PrivateKey::from_bytes(&bouncycastle_hex::decode(D_HEX).unwrap()) + .expect("bc-java's dA is in [1, n-1]"); + let pk = sk.derive_pk(); + assert_eq!(pk, pubkey_from_hex(PA_X, PA_Y), "PA = [dA]G disagrees with the external vector"); + + let (x, y) = (pk.encode()[1..33].to_vec(), pk.encode()[33..65].to_vec()); + let x = bouncycastle_ec::sm2::Sm2FieldElement::from_limbs(sm2_sec1::limbs_from_be_bytes( + &x.try_into().unwrap(), + )); + let y = bouncycastle_ec::sm2::Sm2FieldElement::from_limbs(sm2_sec1::limbs_from_be_bytes( + &y.try_into().unwrap(), + )); + assert_eq!( + bouncycastle_hex::encode(bouncycastle_sm2::za::compute(ID, &x, &y).unwrap()), + ZA_HEX, + "ZA disagrees with the external vector" + ); + + let sig = sig_from_hex(R, S); + pk.verify(MSG, Some(ID), &sig).expect("bc-java's signature must verify"); + + // and it must be specific to that identity and message + assert!(pk.verify(MSG, Some(b"someone-else"), &sig).is_err()); + assert!(pk.verify(b"hi chappy!", Some(ID), &sig).is_err()); +} diff --git a/crypto/sm2/tests/sm2_tests.rs b/crypto/sm2/tests/sm2_tests.rs new file mode 100644 index 000000000..b08aa2415 --- /dev/null +++ b/crypto/sm2/tests/sm2_tests.rs @@ -0,0 +1,473 @@ +//! Behavioural tests for [`SM2`]. Does not reuse +//! `bouncycastle_core_test_framework::signature::TestFrameworkSignature`: that framework calls +//! `sign(&sk, msg, None)` unconditionally in several of its checks (basic sign/verify, `sign_out`, +//! the large-message case), which is incompatible with SM2's requirement that `ctx` always carry +//! the signer's identity `IDA` (see [`crate::sm2`]'s module docs) -- every other primitive the +//! framework covers treats `ctx` as optional. This file instead covers the same ground by hand, +//! always passing `ctx = Some(id)`, plus a dedicated test that `ctx = None` is rejected. + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::signature_key::{SigningKey, VerifyingKey}; +use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle_core::traits::{SignatureSigner, SignatureVerifier}; +use bouncycastle_core_test_framework::DUMMY_SEED; +use bouncycastle_core_test_framework::signature::TestFrameworkSignatureKeys; +use bouncycastle_ec::sm2_scalar::N_LIMBS; +use bouncycastle_ec::sm2_sec1::be_bytes_from_limbs; +use bouncycastle_rng::DefaultRNG; +use bouncycastle_sm2::keys::{SM2PrivateKey, SM2PublicKey, keygen}; + +const ID: &[u8] = b"test-identity@example.com"; + +#[test] +fn key_boundary_conditions() { + TestFrameworkSignatureKeys::new() + .test_keys::(|| keygen(&mut DefaultRNG::default())); +} + +#[test] +fn derive_pk_matches_keygen() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_eq!(sk.derive_pk(), pk); +} + +#[test] +fn sign_then_verify_round_trips() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"The quick brown fox jumped over the lazy dog"; + + let sig = sk.sign(&mut DefaultRNG::default(), msg, Some(ID)).unwrap(); + pk.verify(msg, Some(ID), &sig).unwrap(); +} + +#[test] +fn sign_is_non_deterministic() { + // GB/T 32918.2-2016 S6.1 step A3: k is drawn fresh from randomness each call, unlike + // bouncycastle-ecdsa's RFC-6979-deterministic default. + let (_, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"same message, different k"; + + let sig1 = sk.sign(&mut DefaultRNG::default(), msg, Some(ID)).unwrap(); + let sig2 = sk.sign(&mut DefaultRNG::default(), msg, Some(ID)).unwrap(); + assert_ne!(sig1, sig2); +} + +#[test] +fn sign_and_verify_require_ctx() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, Some(ID)).unwrap(); + + match sk.sign(&mut DefaultRNG::default(), msg, None) { + Err(SignatureError::GenericError(_)) => {} + other => panic!("expected GenericError, got {other:?}"), + } + match pk.verify(msg, None, &sig) { + Err(SignatureError::GenericError(_)) => {} + other => panic!("expected GenericError, got {other:?}"), + } +} + +#[test] +fn different_ctx_values_produce_signatures_valid_only_under_their_own_ctx() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + + let sig_a = sk.sign(&mut DefaultRNG::default(), msg, Some(b"identity-a")).unwrap(); + pk.verify(msg, Some(b"identity-a"), &sig_a).unwrap(); + match pk.verify(msg, Some(b"identity-b"), &sig_a) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("expected SignatureVerificationFailed, got {other:?}"), + } +} + +#[test] +fn verify_rejects_signature_under_wrong_key() { + let (_, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let (other_pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, Some(ID)).unwrap(); + match other_pk.verify(msg, Some(ID), &sig) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("expected SignatureVerificationFailed, got {other:?}"), + } +} + +#[test] +fn verify_rejects_bitflipped_signature() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, Some(ID)).unwrap(); + + for i in 0..sig.len() { + for j in 0..8u8 { + let mut tampered = sig; + tampered[i] ^= 1 << j; + match pk.verify(msg, Some(ID), &tampered) { + Err(SignatureError::SignatureVerificationFailed) => {} + _ => panic!("bit flip at byte {i} bit {j} should have failed verification"), + } + } + } +} + +#[test] +fn sign_randomized_produces_distinct_valid_signatures() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"randomised SM2 signing"; + + let sig1 = sk.sign(&mut DefaultRNG::default(), msg, Some(ID)).unwrap(); + let sig2 = sk.sign(&mut DefaultRNG::default(), msg, Some(ID)).unwrap(); + assert_ne!(sig1, sig2); + + pk.verify(msg, Some(ID), &sig1).unwrap(); + pk.verify(msg, Some(ID), &sig2).unwrap(); +} + +#[test] +fn sign_out_produces_a_verifiable_signature() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + + let mut out = [0xFFu8; 64]; + let written = sk.sign_out(&mut DefaultRNG::default(), msg, Some(ID), &mut out).unwrap(); + assert_eq!(written, 64); + pk.verify(msg, Some(ID), &out).unwrap(); +} + +#[test] +fn large_message_round_trips() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let sig = sk.sign(&mut DefaultRNG::default(), DUMMY_SEED, Some(ID)).unwrap(); + pk.verify(DUMMY_SEED, Some(ID), &sig).unwrap(); +} + +#[test] +fn streaming_sign_and_verify_single_update() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + + let mut s = sk.create_signer(&mut DefaultRNG::default(), Some(ID)).unwrap(); + s.do_update(DUMMY_SEED); + let sig = s.do_final().unwrap(); + + let mut v = pk.create_verifier(Some(ID)).unwrap(); + v.do_update(DUMMY_SEED); + v.do_verify_final(&sig).unwrap(); +} + +#[test] +fn streaming_sign_and_verify_chunked() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + + let mut s = sk.create_signer(&mut DefaultRNG::default(), Some(ID)).unwrap(); + for chunk in DUMMY_SEED.chunks(100) { + s.do_update(chunk); + } + let sig = s.do_final().unwrap(); + + let mut v = pk.create_verifier(Some(ID)).unwrap(); + for chunk in DUMMY_SEED.chunks(100) { + v.do_update(chunk); + } + v.do_verify_final(&sig).unwrap(); +} + +#[test] +fn streaming_verify_rejects_wrong_message() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let sig = sk.sign(&mut DefaultRNG::default(), DUMMY_SEED, Some(ID)).unwrap(); + + let mut v = pk.create_verifier(Some(ID)).unwrap(); + v.do_update(b"this is the wrong message"); + match v.do_verify_final(&sig) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("expected SignatureVerificationFailed, got {other:?}"), + } +} + +#[test] +fn sign_final_out_matches_sign_final() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + + let mut s = sk.create_signer(&mut DefaultRNG::default(), Some(ID)).unwrap(); + s.do_update(DUMMY_SEED); + let mut sig = [0u8; 64]; + let written = s.do_final_out(&mut sig).unwrap(); + assert_eq!(written, 64); + + pk.verify(DUMMY_SEED, Some(ID), &sig).unwrap(); +} + +#[test] +fn verify_rejects_signature_of_the_wrong_length() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, Some(ID)).unwrap(); + pk.verify(msg, Some(ID), &sig).expect("the untampered signature must verify"); + + // Trailing bytes must not be ignored: otherwise one valid signature yields unlimited distinct + // byte strings that all verify, which breaks any caller that compares or deduplicates + // signatures as opaque blobs. + for extra in 1..=3 { + let mut too_long = sig.to_vec(); + too_long.extend(core::iter::repeat_n(0xAAu8, extra)); + match pk.verify(msg, Some(ID), &too_long) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{extra} trailing byte(s) should have failed, got {other:?}"), + } + } + + for short in 1..=3 { + match pk.verify(msg, Some(ID), &sig[..64 - short]) { + Err(SignatureError::LengthError(_)) => {} + other => panic!("{short} byte(s) short should have failed, got {other:?}"), + } + } +} + +#[test] +fn public_key_uncompressed_and_compressed_round_trip() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let uncompressed = pk.encode(); + assert_eq!(uncompressed[0], 0x04); + assert_eq!(SM2PublicKey::from_bytes(&uncompressed).unwrap(), pk); + + let compressed_tag = if uncompressed[64] & 1 == 0 { 0x02 } else { 0x03 }; + let mut compressed = [0u8; 33]; + compressed[0] = compressed_tag; + compressed[1..].copy_from_slice(&uncompressed[1..33]); + assert_eq!(SM2PublicKey::from_bytes(&compressed).unwrap(), pk); +} + +#[test] +fn private_key_encode_round_trips_exact_bytes() { + let mut bytes = [0u8; 32]; + bytes[31] = 0x2A; // d = 42, well within [1, n-1] + let sk = SM2PrivateKey::from_bytes(&bytes).unwrap(); + assert_eq!(sk.encode(), bytes); + + let mut out = [0xFFu8; 32]; + let written = sk.encode_out(&mut out); + assert_eq!(written, 32); + assert_eq!(out, bytes); +} + +#[test] +fn public_key_encode_out_matches_encode() { + let (pk, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let encoded = pk.encode(); + + let mut out = [0xFFu8; 65]; + let written = pk.encode_out(&mut out); + assert_eq!(written, 65); + assert_eq!(out, encoded); +} + +#[test] +fn public_key_display_differs_for_different_keys() { + let (pk1, _) = keygen(&mut DefaultRNG::default()).unwrap(); + let (pk2, _) = keygen(&mut DefaultRNG::default()).unwrap(); + assert_ne!(format!("{pk1}"), format!("{pk2}")); +} + +#[test] +fn private_key_rejects_zero() { + match SM2PrivateKey::from_bytes(&[0u8; 32]) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("expected DecodingError, got {other:?}"), + } +} + +/// GB/T 32918.1-2016 §6.1 a) puts `dA` in `[1, n-2]`, so an encoding of `n - 1` or above is not +/// a key. `n - 1` cannot sign (step A6 would invert `1 + dA = n`, i.e. `0`), and reducing an +/// encoding of `n` or above mod `n` rather than rejecting it would give one key many +/// valid-looking encodings: `dA = n + 1` would load as `dA = 1`. `n - 2` is the top of the range +/// and the one value for which step A6 divides by `n - 1`, i.e. by `-1`. +#[test] +fn private_key_rejects_values_outside_1_to_n_minus_2() { + let n = be_bytes_from_limbs(&N_LIMBS); + + // N_LIMBS[0] ends 0x23, so none of the +-1/-2 adjustments below carries or borrows. + let mut n_minus_1 = n; + n_minus_1[31] -= 1; + let mut n_plus_1 = n; + n_plus_1[31] += 1; + for (name, bytes) in + [("n - 1", n_minus_1), ("n", n), ("n + 1", n_plus_1), ("2^256 - 1", [0xffu8; 32])] + { + match SM2PrivateKey::from_bytes(&bytes) { + Err(SignatureError::DecodingError(_)) => {} + other => panic!("dA = {name} should have been rejected, got {other:?}"), + } + } + + let mut n_minus_2 = n; + n_minus_2[31] -= 2; + let sk = SM2PrivateKey::from_bytes(&n_minus_2).expect("dA = n - 2 is in range"); + assert_eq!(sk.encode(), n_minus_2, "dA = n - 2 must round-trip unchanged"); + + let msg = b"dA = n - 2: step A6 divides by 1 + dA = n - 1"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, Some(ID)).expect("dA = n - 2 must sign"); + sk.derive_pk().verify(msg, Some(ID), &sig).expect("and its signature must verify"); +} + +#[test] +fn keygen_is_deterministic_given_a_deterministic_rng() { + struct FixedRng; + impl RNG for FixedRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0x42424242) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0x42u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0x42); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::_256bit + } + } + + let (pk1, sk1) = bouncycastle_sm2::keys::keygen(&mut FixedRng).unwrap(); + let (pk2, sk2) = bouncycastle_sm2::keys::keygen(&mut FixedRng).unwrap(); + assert_eq!(sk1, sk2); + assert_eq!(pk1, pk2); +} + +/// FIPS 186-5 Appendix A.2.1 step 3: an RNG weaker than the 128-bit security strength +/// SM2's order calls for (SP 800-57 Part 1 Rev. 5, Table 2) is refused rather than silently +/// used. +#[test] +fn keygen_rejects_an_rng_below_the_required_strength() { + struct WeakRng; + impl RNG for WeakRng { + fn add_seed_keymaterial( + &mut self, + _additional_seed: &dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result<(), bouncycastle_core::errors::RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Ok(0) + } + fn next_bytes( + &mut self, + len: usize, + ) -> Result, bouncycastle_core::errors::RNGError> { + Ok(vec![0u8; len]) + } + fn next_bytes_out( + &mut self, + out: &mut [u8], + ) -> Result { + out.fill(0); + Ok(out.len()) + } + fn fill_keymaterial_out( + &mut self, + _out: &mut dyn bouncycastle_core::key_material::KeyMaterialTrait, + ) -> Result { + unimplemented!() + } + fn security_strength(&self) -> bouncycastle_core::security_strength::SecurityStrength { + bouncycastle_core::security_strength::SecurityStrength::None + } + } + + assert!(matches!( + bouncycastle_sm2::keys::keygen(&mut WeakRng), + Err(SignatureError::RNGError( + bouncycastle_core::errors::RNGError::SecurityStrengthInsufficientForAlgorithm + )) + )); +} + +/// GB/T 32918.2-2016 §7.1 steps B1-B2: `r'` and `s'` must each be in `[1, n-1]`, rejected +/// rather than reduced. Each half of a valid signature is replaced in turn by `0`, by `n`, and by +/// the all-ones pattern; the untouched half stays valid, so only the range check can be what +/// rejects the result. +#[test] +fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"range-checked r and s"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, Some(ID)).unwrap(); + pk.verify(msg, Some(ID), &sig).expect("the untampered signature must verify"); + + let n = be_bytes_from_limbs(&N_LIMBS); + for (name, bad) in [("0", [0u8; 32]), ("n", n), ("2^256 - 1", [0xffu8; 32])] { + for (half, offset) in [("r", 0usize), ("s", 32usize)] { + let mut tampered = sig; + tampered[offset..offset + 32].copy_from_slice(&bad); + match pk.verify(msg, Some(ID), &tampered) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("{half} = {name} should have been rejected, got {other:?}"), + } + } + } +} + +/// GB/T 32918.2-2016 §7.1 step B5: `t = (r' + s') mod n`, and `t = 0` fails verification +/// outright. `s' = n - r'` is in `[1, n-1]` (so it passes step B2) and is the one value that makes +/// `t` zero -- a signature that would otherwise reach the point multiplication with `[0]PA`. +#[test] +fn verify_rejects_s_equal_to_n_minus_r() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"t = r + s must not be zero"; + let sig = sk.sign(&mut DefaultRNG::default(), msg, Some(ID)).unwrap(); + + let r_limbs = bouncycastle_ec::sm2_sec1::limbs_from_be_bytes(&sig[..32].try_into().unwrap()); + let (n_minus_r, borrow) = bouncycastle_ec::nat::sub(&N_LIMBS, &r_limbs); + assert_eq!(borrow, 0, "r is in [1, n-1], so n - r never borrows"); + + let mut tampered = sig; + tampered[32..].copy_from_slice(&be_bytes_from_limbs(&n_minus_r)); + match pk.verify(msg, Some(ID), &tampered) { + Err(SignatureError::SignatureVerificationFailed) => {} + other => panic!("expected SignatureVerificationFailed, got {other:?}"), + } +} + +/// `ZA` encodes `IDA`'s bit length as the two-byte `ENTLA` (GB/T 32918.2-2016 §5.5), so an +/// identity of 8191 bytes +/// (65528 bits) is the longest that fits and 8192 bytes (65536 bits) is the shortest that does +/// not. The limit applies on both sides, since verification computes `ZA` too. +#[test] +fn identity_longer_than_entla_can_encode_is_rejected() { + let (pk, sk) = keygen(&mut DefaultRNG::default()).unwrap(); + let msg = b"message"; + + let longest = vec![0x41u8; 8191]; + let sig = + sk.sign(&mut DefaultRNG::default(), msg, Some(&longest)).expect("8191 bytes fits in ENTLA"); + pk.verify(msg, Some(&longest), &sig).unwrap(); + + let too_long = vec![0x41u8; 8192]; + match sk.sign(&mut DefaultRNG::default(), msg, Some(&too_long)) { + Err(SignatureError::GenericError(_)) => {} + other => panic!("expected GenericError on sign, got {other:?}"), + } + match pk.verify(msg, Some(&too_long), &sig) { + Err(SignatureError::GenericError(_)) => {} + other => panic!("expected GenericError on verify, got {other:?}"), + } +} diff --git a/crypto/utils/src/ct.rs b/crypto/utils/src/ct.rs index 027894491..3957fdb34 100644 --- a/crypto/utils/src/ct.rs +++ b/crypto/utils/src/ct.rs @@ -38,8 +38,10 @@ where // could otherwise prove to be one of a small number of constants (a `Condition` mask is all-ones // or all-zeros; the accumulator of a comparison loop is zero until the first difference). Given // that knowledge the compiler is free to lower an expression like `(t & m) | (f & !m)` to a branch -// or a conditional move on the secret, or to leave a comparison loop early. To stop that, the value -// is routed through a volatile store and load. +// or a conditional move on the secret, or to leave a comparison loop early. A branch on a mask is +// the same risk in another form: the compiler may replace one test of a combined mask with a test +// per condition that built it. To stop that, the value is routed through a volatile store and +// load. // // This is still a best-effort, not a guarantee. // The language guarantees less than is relied on here. The documentation (as of rust 1.99.0) of @@ -83,8 +85,8 @@ where MaskType: SupportedMaskType, { /// The mask after the optimisation barrier (section comment above), applied at the point of - /// use by every consumer that does masked arithmetic. Not `const`: volatile accesses are not - /// allowed in const context. + /// use by every consumer that does masked arithmetic, and by `to_bool`. Not `const`: volatile + /// accesses are not allowed in const context. #[inline(always)] fn barrier(self) -> Self { Self(value_barrier(self.0)) @@ -214,10 +216,15 @@ impl Condition { let t = (lhs ^ rhs) & self.barrier().0; (lhs ^ t, rhs ^ t) } - /// Convert the mask to a runtime boolean. Only use this at genuine public - /// decision points: branching on the result leaks the condition's value. - pub const fn to_bool(self) -> bool { - self.0 != 0 + /// Convert the mask to a runtime boolean, at a decision whose outcome may be public. The test + /// is made on the whole mask, so the compiler is not given the values that built the mask to + /// branch on separately (best-effort, like every constant-time construction in this crate). If + /// the condition's value itself must stay secret, do not convert it: keep the mask and use + /// [`Self::select`]. + pub fn to_bool(self) -> bool { + // The barrier (section comment above) is what stops LLVM from folding the mask away and + // branching on its constituents; it is volatile, hence this is not `const`. + self.barrier().0 != 0 } } @@ -340,10 +347,15 @@ impl Condition { let t = (lhs ^ rhs) & self.barrier().0; (lhs ^ t, rhs ^ t) } - /// Convert the mask to a runtime boolean. Only use this at genuine public - /// decision points: branching on the result leaks the condition's value. - pub const fn to_bool(self) -> bool { - self.0 != 0 + /// Convert the mask to a runtime boolean, at a decision whose outcome may be public. The test + /// is made on the whole mask, so the compiler is not given the values that built the mask to + /// branch on separately (best-effort, like every constant-time construction in this crate). If + /// the condition's value itself must stay secret, do not convert it: keep the mask and use + /// [`Self::select`]. + pub fn to_bool(self) -> bool { + // The barrier (section comment above) is what stops LLVM from folding the mask away and + // branching on its constituents; it is volatile, hence this is not `const`. + self.barrier().0 != 0 } } @@ -436,10 +448,15 @@ impl Condition { let t = (lhs ^ rhs) & self.barrier().0; (lhs ^ t, rhs ^ t) } - /// Convert the mask to a runtime boolean. Only use this at genuine public - /// decision points: branching on the result leaks the condition's value. - pub const fn to_bool(self) -> bool { - self.0 != 0 + /// Convert the mask to a runtime boolean, at a decision whose outcome may be public. The test + /// is made on the whole mask, so the compiler is not given the values that built the mask to + /// branch on separately (best-effort, like every constant-time construction in this crate). If + /// the condition's value itself must stay secret, do not convert it: keep the mask and use + /// [`Self::select`]. + pub fn to_bool(self) -> bool { + // The barrier (section comment above) is what stops LLVM from folding the mask away and + // branching on its constituents; it is volatile, hence this is not `const`. + self.barrier().0 != 0 } } @@ -521,10 +538,15 @@ impl Condition { let t = (lhs ^ rhs) & self.barrier().0; (lhs ^ t, rhs ^ t) } - /// Convert the mask to a runtime boolean. Only use this at genuine public - /// decision points: branching on the result leaks the condition's value. - pub const fn to_bool(self) -> bool { - self.0 != 0 + /// Convert the mask to a runtime boolean, at a decision whose outcome may be public. The test + /// is made on the whole mask, so the compiler is not given the values that built the mask to + /// branch on separately (best-effort, like every constant-time construction in this crate). If + /// the condition's value itself must stay secret, do not convert it: keep the mask and use + /// [`Self::select`]. + pub fn to_bool(self) -> bool { + // The barrier (section comment above) is what stops LLVM from folding the mask away and + // branching on its constituents; it is volatile, hence this is not `const`. + self.barrier().0 != 0 } } @@ -705,3 +727,27 @@ pub fn conditional_copy_bytes( *o = (x & mask) | (y & !mask); } } + +/// Conditionally selects between two limb arrays without branching: writes `a` into `out` if +/// `cond` is TRUE, otherwise `b`. Every limb is written under the mask, regardless of `cond`, so +/// the operation takes the same path irrespective of which array is selected. +pub fn conditional_select( + cond: Condition, + a: &[u64; L], + b: &[u64; L], + out: &mut [u64; L], +) { + for i in 0..L { + out[i] = cond.select(a[i], b[i]); + } +} + +/// Conditionally swaps two limb arrays in place without branching: if `cond` is TRUE, `a` and +/// `b` are exchanged; otherwise both are left unchanged. Every limb is written either way. +pub fn conditional_swap(cond: Condition, a: &mut [u64; L], b: &mut [u64; L]) { + for i in 0..L { + let (new_a, new_b) = cond.swap(a[i], b[i]); + a[i] = new_a; + b[i] = new_b; + } +} diff --git a/crypto/utils/src/secret.rs b/crypto/utils/src/secret.rs index 8d0d4bacd..bdef0e8a9 100644 --- a/crypto/utils/src/secret.rs +++ b/crypto/utils/src/secret.rs @@ -292,6 +292,39 @@ impl Default for Secret { } } +/// Overwrites `value` with its all-zero representation through a volatile write, which the +/// compiler is not permitted to elide as a dead store (see this module's docs on why a plain +/// `fill(0)` is not enough). +/// +/// [`Secret`] is the right tool whenever a secret can *live* inside one: it scrubs on drop, so a +/// caller cannot forget to. This is the escape hatch for values that cannot -- a field of a `Copy` +/// type whose public API depends on staying `Copy`, for instance, where wrapping it would be a +/// breaking change out of proportion to the gain. The caller then owns the responsibility for +/// calling this at the right moment, and on every path out of the function. Prefer [`Secret`] +/// wherever the choice exists. +/// +/// This lives here rather than in the crates that need it because those crates are +/// `#![forbid(unsafe_code)]`; keeping the one `write_volatile` central is this crate's stated +/// purpose. +pub fn zeroize_in_place(value: &mut T) { + // Just to make sure -- this should trigger on any unit tests for any instantiation of `T` + // that causes this assumption to be violated. + debug_assert_eq!(size_of::(), size_of_val(&T::ZEROED)); + + // SAFETY: `value` is a valid, properly aligned, mutable reference to an initialized `T`, which + // is exactly the contract `write_volatile` requires. `T::ZEROED` is defined by + // `ZeroizablePrimitive` as the all-zero value of `T`, a valid and correctly-sized bit pattern. + // `write_volatile` (rather than a plain store) is what forbids the compiler from eliding this + // scrub as a dead write, per its contract: + // https://doc.rust-lang.org/std/ptr/fn.write_volatile.html + unsafe { + ptr::write_volatile(value, T::ZEROED); + } + // Compile-time barrier: keeps the volatile scrub ordered before any later memory ops. Emits no + // machine instructions. + compiler_fence(Ordering::SeqCst); +} + impl Secret { /// Securely overwrite the contained value with zeros. /// After this returns, every byte of the wrapped value has been volatile-written to `0`. @@ -303,21 +336,7 @@ impl Secret { /// since this will still call `zeroize()` and also move the object, preventing you from accidentally reusing it. #[inline] pub fn zeroize(&mut self) { - // SAFETY: `&mut self.0` is a valid, properly aligned, mutable reference to an initialized - // `T`, which is exactly the contract `write_volatile` requires. - // `T::ZEROED` is a valid value of `T`, so it is a valid value to write. - //`write_volatile` (rather than a plain store) is what forbids the compiler from eliding - // this scrub as a dead write as per its contract: - // https://doc.rust-lang.org/std/ptr/fn.write_volatile.html - - unsafe { - ptr::write_volatile(&mut self.0, T::ZEROED); - } - // Compile-time barrier: keeps the volatile scrub ordered before any later memory ops. - // (for example, if the user calls .zeroize() outside of a drop and then continues using - // the object by filling it with new data, which is valid usage.) - // Emits no machine instructions. - compiler_fence(Ordering::SeqCst); + zeroize_in_place(&mut self.0); } } diff --git a/crypto/utils/tests/ct_tests.rs b/crypto/utils/tests/ct_tests.rs index fe85e1385..ed6153241 100644 --- a/crypto/utils/tests/ct_tests.rs +++ b/crypto/utils/tests/ct_tests.rs @@ -1053,3 +1053,40 @@ mod ct_bytes_tests { // conditional_copy_bytes(&a, &c, &mut out, false); } } + +mod limb_array_tests { + use bouncycastle_utils::ct::Condition; + + #[test] + fn test_conditional_select() { + use bouncycastle_utils::ct::conditional_select; + + let a: [u64; 4] = [0x01, 0x02, 0x03, 0x04]; + let b: [u64; 4] = [u64::MAX, u64::MAX - 1, u64::MAX - 2, u64::MAX - 3]; + let mut out = [0u64; 4]; + + conditional_select(Condition::::TRUE, &a, &b, &mut out); + assert_eq!(out, a); + + conditional_select(Condition::::FALSE, &a, &b, &mut out); + assert_eq!(out, b); + } + + #[test] + fn test_conditional_swap() { + use bouncycastle_utils::ct::conditional_swap; + + let a_orig: [u64; 4] = [0x01, 0x02, 0x03, 0x04]; + let b_orig: [u64; 4] = [u64::MAX, u64::MAX - 1, u64::MAX - 2, u64::MAX - 3]; + + let mut a = a_orig; + let mut b = b_orig; + conditional_swap(Condition::::FALSE, &mut a, &mut b); + assert_eq!(a, a_orig); + assert_eq!(b, b_orig); + + conditional_swap(Condition::::TRUE, &mut a, &mut b); + assert_eq!(a, b_orig); + assert_eq!(b, a_orig); + } +} diff --git a/dev_scripts/cli_ccm_cfb_examples.sh b/dev_scripts/cli_ccm_cfb_examples.sh new file mode 100755 index 000000000..08348e65a --- /dev/null +++ b/dev_scripts/cli_ccm_cfb_examples.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# Builds the `bc-rust` CLI and runs it through AES-CCM, AES-CFB128 and AES-CFB8 examples. +# +# Each example is checked, not just shown: CCM against SP 800-38C Appendix C Examples 1 and 2 +# (nonce supplied, so the output is a known answer) and the round trips, AAD binding and tamper +# detection the subcommand help promises; CFB128 and CFB8 against SP 800-38A Appendix F.3 with +# the IV supplied through `--iv`, and with the default generated IV for round trip, output +# length, pipeline composition and the tamper-propagation behaviour each mode's help describes. +# +# Usage: ./dev_scripts/cli_ccm_cfb_examples.sh [--release] +# Exit status is non-zero if any check fails. Needs `xxd` and `cmp`. + +set -u +cd "$(dirname "$0")/.." + +PROFILE=debug +CARGO_FLAGS=() +if [ "${1:-}" = "--release" ]; then + PROFILE=release + CARGO_FLAGS=(--release) +fi + +export PATH="$HOME/.cargo/bin:$PATH" +echo "## building bc-rust ($PROFILE)" +cargo build -p cli "${CARGO_FLAGS[@]}" || exit 1 +BC="./target/$PROFILE/bc-rust" + +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT + +FAILURES=0 +pass() { printf ' ok %s\n' "$1"; } +fail() { printf ' FAIL %s\n' "$1"; FAILURES=$((FAILURES + 1)); } +check() { # check ; passes if the command exits 0 + local desc=$1; shift + if "$@"; then pass "$desc"; else fail "$desc"; fi +} +hex() { xxd -p | tr -d '\n'; } +unhex() { xxd -r -p; } + +# A throwaway key for the round-trip examples. Never put a real key on a command line; the +# commands take `--key-file` for that, and the CFB examples below use it. +KEY=000102030405060708090a0b0c0d0e0f +KEY_FILE="$WORK/key.bin" +echo -n "$KEY" | unhex > "$KEY_FILE" + +############################################################################################### +echo +echo "## AES-128-CCM: known answers from SP 800-38C Appendix C" +echo "# (nonce is supplied, not generated, so the output is reproducible)" + +# C.1 Example 1: Klen = 128, Tlen = 32, Nlen = 56, Alen = 64, Plen = 32. +K1=404142434445464748494a4b4c4d4e4f +N1=10111213141516 +A1=0001020304050607 +P1=20212223 +C1=7162015b4dac255d # ciphertext || 4-byte tag +out=$(echo -n "$P1" | unhex | $BC aes128-ccm -d encrypt --key $K1 --nonce $N1 --aad $A1 --tag-len 4 -x) +check "Example 1 encrypt: $out" [ "$out" = "$C1" ] +out=$(echo -n "$C1" | unhex | $BC aes128-ccm -d decrypt --key $K1 --nonce $N1 --aad $A1 --tag-len 4 -x) +check "Example 1 decrypt: $out" [ "$out" = "$P1" ] + +# C.2 Example 2: Klen = 128, Tlen = 48, Nlen = 64, Alen = 128, Plen = 128. +N2=1011121314151617 +A2=000102030405060708090a0b0c0d0e0f +P2=202122232425262728292a2b2c2d2e2f +C2=d2a1f0e051ea5f62081a7792073d593d1fc64fbfaccd # ciphertext || 6-byte tag +out=$(echo -n "$P2" | unhex | $BC aes128-ccm -d encrypt --key $K1 --nonce $N2 --aad $A2 --tag-len 6 -x) +check "Example 2 encrypt: $out" [ "$out" = "$C2" ] +out=$(echo -n "$C2" | unhex | $BC aes128-ccm -d decrypt --key $K1 --nonce $N2 --aad $A2 --tag-len 6 -x) +check "Example 2 decrypt: $out" [ "$out" = "$P2" ] + +echo +echo "## AES-128-CCM: round trip, AAD binding, tamper detection (12-byte nonce, 16-byte tag)" +NONCE=000000000000000000000001 # a message counter; MUST be unique per key +MSG="$WORK/msg.txt" +printf 'The quick brown fox jumps over the lazy dog. %s\n' "$(date -u +%FT%TZ)" > "$MSG" +AAD_FILE="$WORK/aad.bin" +printf 'header: v1, msg 1' > "$AAD_FILE" + +$BC aes128-ccm -d encrypt -k "$KEY_FILE" --nonce $NONCE --aad-file "$AAD_FILE" < "$MSG" > "$WORK/msg.ccm" +check "ciphertext is input + 16 tag bytes ($(stat -c %s "$WORK/msg.ccm") = $(stat -c %s "$MSG") + 16)" \ + [ "$(stat -c %s "$WORK/msg.ccm")" -eq $(( $(stat -c %s "$MSG") + 16 )) ] + +$BC aes128-ccm -d decrypt -k "$KEY_FILE" --nonce $NONCE --aad-file "$AAD_FILE" < "$WORK/msg.ccm" > "$WORK/msg.out" +check "decrypt round-trips" cmp -s "$MSG" "$WORK/msg.out" + +# AAD is authenticated: a different AAD must fail, and must write nothing. +if $BC aes128-ccm -d decrypt -k "$KEY_FILE" --nonce $NONCE --aad 00 < "$WORK/msg.ccm" > "$WORK/bad.out" 2>/dev/null; then + fail "decrypt with the wrong AAD was accepted" +else + check "decrypt with the wrong AAD fails and writes no plaintext" [ ! -s "$WORK/bad.out" ] +fi + +# Tamper: flip one bit of the first ciphertext byte. +python3 -c " +import sys; d = bytearray(open(sys.argv[1], 'rb').read()); d[0] ^= 1; open(sys.argv[2], 'wb').write(d) +" "$WORK/msg.ccm" "$WORK/msg.tampered" +if $BC aes128-ccm -d decrypt -k "$KEY_FILE" --nonce $NONCE --aad-file "$AAD_FILE" < "$WORK/msg.tampered" > "$WORK/bad.out" 2>/dev/null; then + fail "tampered ciphertext was accepted" +else + check "tampered ciphertext fails and writes no plaintext" [ ! -s "$WORK/bad.out" ] +fi + +# Shorter tag, 7-byte nonce (the largest payload limit), encrypt | decrypt in one pipeline. +out=$($BC aes128-ccm -d encrypt -k "$KEY_FILE" --nonce 01020304050607 --tag-len 8 < "$MSG" \ + | $BC aes128-ccm -d decrypt -k "$KEY_FILE" --nonce 01020304050607 --tag-len 8) +check "7-byte nonce, 8-byte tag, encrypt | decrypt pipeline round-trips" [ "$out" = "$(cat "$MSG")" ] + +############################################################################################### +# SP 800-38A Appendix F.3, AES-128: the same key and IV for both modes; CFB8 uses the first 18 +# plaintext bytes (F.3.7, 18 segments), CFB128 all 64 (F.3.13, 4 segments). +K38A=2b7e151628aed2a6abf7158809cf4f3c +IV38A=000102030405060708090a0b0c0d0e0f +P38A=6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710 +C_CFB=3b3fd92eb72dad20333449f8e83cfb4ac8a64537a0b3a93fcde3cdad9f1ce58b26751f67a3cbb140b1808cf187a4f4dfc04b05357c5d1c0eeac4c66f9ff7f2e6 +P_CFB8=${P38A:0:36} +C_CFB8=3b79424c9c0dd436bace9e0ed4586a4f32b9 + +for MODE in cfb cfb8; do + CMD="aes128-$MODE" + if [ "$MODE" = cfb ]; then P=$P38A; C=$C_CFB; SEC="F.3.13 / F.3.14"; else P=$P_CFB8; C=$C_CFB8; SEC="F.3.7 / F.3.8"; fi + + echo + echo "## AES-128-${MODE^^}: known answers from SP 800-38A Appendix $SEC, IV supplied with --iv" + echo "# (with --iv, encrypt writes only ciphertext and decrypt reads only ciphertext)" + out=$(echo -n "$P" | unhex | $BC $CMD -d encrypt --key $K38A --iv $IV38A -x) + check "encrypt: $out" [ "$out" = "$C" ] + out=$(echo -n "$C" | unhex | $BC $CMD -d decrypt --key $K38A --iv $IV38A -x) + check "decrypt: $out" [ "$out" = "$P" ] + + # The same IV from a file of raw bytes, and --iv on both ends of a pipeline. + echo -n "$IV38A" | unhex > "$WORK/iv.bin" + out=$(echo -n "$P" | unhex | $BC $CMD -d encrypt --key $K38A --iv-file "$WORK/iv.bin" -x) + check "encrypt with --iv-file gives the same answer" [ "$out" = "$C" ] + out=$($BC $CMD -d encrypt -k "$KEY_FILE" --iv $IV38A < "$MSG" | $BC $CMD -d decrypt -k "$KEY_FILE" --iv $IV38A) + check "encrypt --iv | decrypt --iv round-trips, ciphertext only in the pipe" [ "$out" = "$(cat "$MSG")" ] + check "ciphertext with --iv is exactly the input length" \ + [ "$($BC $CMD -d encrypt -k "$KEY_FILE" --iv $IV38A < "$MSG" | wc -c)" -eq "$(stat -c %s "$MSG")" ] + + # A wrong-length IV is refused before anything is read. + if echo -n 00 | $BC $CMD -d encrypt --key $K38A --iv 0001 > /dev/null 2>&1; then + fail "a 2-byte IV was accepted" + else + pass "a 2-byte IV is refused" + fi + + echo + echo "## AES-128-${MODE^^}: default, IV generated and prepended (first 16 bytes of output)" + + $BC $CMD -d encrypt -k "$KEY_FILE" < "$MSG" > "$WORK/msg.$MODE" + check "ciphertext is 16-byte IV + input ($(stat -c %s "$WORK/msg.$MODE") = 16 + $(stat -c %s "$MSG"))" \ + [ "$(stat -c %s "$WORK/msg.$MODE")" -eq $(( 16 + $(stat -c %s "$MSG") )) ] + + $BC $CMD -d decrypt -k "$KEY_FILE" < "$WORK/msg.$MODE" > "$WORK/msg.out" + check "decrypt round-trips" cmp -s "$MSG" "$WORK/msg.out" + + # Two encryptions of the same message differ: the IV is fresh each time. + $BC $CMD -d encrypt -k "$KEY_FILE" < "$MSG" > "$WORK/msg2.$MODE" + check "a second encryption has a different IV and ciphertext" [ "$(hex < "$WORK/msg.$MODE")" != "$(hex < "$WORK/msg2.$MODE")" ] + + # Pipeline: encrypt | decrypt with no file in between, any length (here 1 byte and 100 KiB). + out=$(printf 'x' | $BC $CMD -d encrypt -k "$KEY_FILE" | $BC $CMD -d decrypt -k "$KEY_FILE") + check "1-byte message through an encrypt | decrypt pipeline" [ "$out" = "x" ] + head -c 102400 /dev/urandom > "$WORK/big.bin" + $BC $CMD -d encrypt -k "$KEY_FILE" < "$WORK/big.bin" | $BC $CMD -d decrypt -k "$KEY_FILE" > "$WORK/big.out" + check "100 KiB through an encrypt | decrypt pipeline" cmp -s "$WORK/big.bin" "$WORK/big.out" + + # No integrity: a flipped ciphertext bit decrypts "successfully" to corrupted plaintext. + # CFB128: the same bit of the same block flips and the NEXT block is garbled, then it resyncs. + # CFB8: the same bit of the same byte flips and the next 16 bytes are garbled, then it resyncs. + # Flip bit 0 of plaintext byte 20 (ciphertext byte 16 + 20). + python3 -c " +import sys; d = bytearray(open(sys.argv[1], 'rb').read()); d[16 + 20] ^= 1; open(sys.argv[2], 'wb').write(d) +" "$WORK/msg.$MODE" "$WORK/msg.$MODE.tampered" + $BC $CMD -d decrypt -k "$KEY_FILE" < "$WORK/msg.$MODE.tampered" > "$WORK/tampered.out" + check "tampering is NOT detected (exit 0, output produced)" [ -s "$WORK/tampered.out" ] + if [ "$MODE" = cfb ]; then garble_from=32; garble_to=48; else garble_from=21; garble_to=37; fi + python3 - "$MSG" "$WORK/tampered.out" $garble_from $garble_to <<'EOF' +import sys +a = open(sys.argv[1], 'rb').read(); b = open(sys.argv[2], 'rb').read() +lo, hi = int(sys.argv[3]), int(sys.argv[4]) +diff = [i for i in range(len(a)) if a[i] != b[i]] +assert len(a) == len(b), "length changed" +assert diff[0] == 20 and a[20] ^ b[20] == 1, f"byte 20 should differ by exactly bit 0, got {diff[:3]}" +assert all(lo <= i < hi for i in diff[1:]), f"corruption outside [{lo},{hi}): {diff}" +assert all(a[i] == b[i] for i in range(hi, len(a))), "did not resynchronise" +print(f" ok flipped bit lands on plaintext byte 20, bytes {lo}..{hi-1} garbled, then resynchronised") +EOF + [ $? -eq 0 ] || fail "tamper propagation did not match the mode's description" +done + +############################################################################################### +echo +if [ $FAILURES -eq 0 ]; then + echo "all checks passed" +else + echo "$FAILURES check(s) FAILED" + exit 1 +fi diff --git a/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index 1e06763dc..6eb542fc7 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -31,6 +31,18 @@ path = "src/bench_aes_mem_usage.rs" name = "bench_ccm_mem_usage" path = "src/bench_ccm_mem_usage.rs" +[[bin]] +name = "bench_ecdsa_mem_usage" +path = "src/bench_ecdsa_mem_usage.rs" + +[[bin]] +name = "bench_sm2_mem_usage" +path = "src/bench_sm2_mem_usage.rs" + +[[bin]] +name = "bench_rsa_mem_usage" +path = "src/bench_rsa_mem_usage.rs" + [[bin]] name = "bench_ascon_mem_usage" path = "src/bench_ascon_mem_usage.rs" diff --git a/mem_usage_benches/src/bench_ecdsa_mem_usage.rs b/mem_usage_benches/src/bench_ecdsa_mem_usage.rs new file mode 100644 index 000000000..27348c43e --- /dev/null +++ b/mem_usage_benches/src/bench_ecdsa_mem_usage.rs @@ -0,0 +1,474 @@ +//! 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_ecdsa_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_ecdsa_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ``` +//! +//! Make sure you build in release mode! +//! +//! The code is using print!() to force the compiler not to optimize away the actual code. +//! It is printing important outputs for benchmarking to stderr so that the rest can be mapped to /dev/null +//! (this is because /usr/bin/time prints useful outputs to stderr as well) +//! +//! Main is at the bottom, controls which this was actually run. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use bouncycastle::core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle::core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle::core::traits::{SignatureSigner, SignatureVerifier}; +use bouncycastle::ecdsa::keys_common::DerivePublicKey; +use bouncycastle::ecdsa::{ + ecdsa_bp256r1::ECDSABp256r1, ecdsa_bp384r1::ECDSABp384r1, ecdsa_bp512r1::ECDSABp512r1, + ecdsa_p256::ECDSAP256, ecdsa_p256k1::ECDSASecp256K1, ecdsa_p384::ECDSAP384, + ecdsa_p521::ECDSAP521, +}; +use bouncycastle::ecdsa::{ + keys_bp256r1, keys_bp384r1, keys_bp512r1, keys_p256, keys_p256k1, keys_p384, keys_p521, +}; +use bouncycastle::rng::DefaultRNG; + +/// A fixed message, reused across every curve's sign/verify bench so peak stack usage reflects +/// only the signature algorithm, not message size. +const MSG: &[u8] = b"peak stack usage of ECDSA sign/verify, held constant across every curve"; + +/// This prints the in-memory size of all the public and private key structs, plus their on-disk +/// encoded length, for every curve. +fn print_struct_sizes() { + use core::mem::size_of; + + println!("\nECDSA P-256"); + println!("size_of: {}", size_of::()); + println!("PK_LEN (on disk): {}", keys_p256::PK_LEN); + println!("size_of: {}", size_of::()); + println!("SK_LEN (on disk): {}", keys_p256::SK_LEN); + + println!("\nECDSA P-384"); + println!("size_of: {}", size_of::()); + println!("PK_LEN (on disk): {}", keys_p384::PK_LEN); + println!("size_of: {}", size_of::()); + println!("SK_LEN (on disk): {}", keys_p384::SK_LEN); + + println!("\nECDSA P-521"); + println!("size_of: {}", size_of::()); + println!("PK_LEN (on disk): {}", keys_p521::PK_LEN); + println!("size_of: {}", size_of::()); + println!("SK_LEN (on disk): {}", keys_p521::SK_LEN); + + println!("\nECDSA secp256k1"); + println!( + "size_of: {}", + size_of::() + ); + println!("PK_LEN (on disk): {}", keys_p256k1::PK_LEN); + println!( + "size_of: {}", + size_of::() + ); + println!("SK_LEN (on disk): {}", keys_p256k1::SK_LEN); + + println!("\nECDSA brainpoolP256r1"); + println!( + "size_of: {}", + size_of::() + ); + println!("PK_LEN (on disk): {}", keys_bp256r1::PK_LEN); + println!( + "size_of: {}", + size_of::() + ); + println!("SK_LEN (on disk): {}", keys_bp256r1::SK_LEN); + + println!("\nECDSA brainpoolP384r1"); + println!( + "size_of: {}", + size_of::() + ); + println!("PK_LEN (on disk): {}", keys_bp384r1::PK_LEN); + println!( + "size_of: {}", + size_of::() + ); + println!("SK_LEN (on disk): {}", keys_bp384r1::SK_LEN); + + println!("\nECDSA brainpoolP512r1"); + println!( + "size_of: {}", + size_of::() + ); + println!("PK_LEN (on disk): {}", keys_bp512r1::PK_LEN); + println!( + "size_of: {}", + size_of::() + ); + println!("SK_LEN (on disk): {}", keys_bp512r1::SK_LEN); +} + +/// This exists that /usr/bin/time can be used to measure the base memory footprint of the cargo bench harness +fn bench_do_nothing() { + eprintln!("DoNothing"); + + print!("{}", 1 + 1); +} + +fn bench_p256_keygen() { + eprintln!("ECDSA P-256/KeyGen"); + + let (pk, _sk) = keys_p256::keygen(&mut DefaultRNG::default()).unwrap(); + println!("{:x?}", pk.encode()); +} + +fn bench_p256_sign() { + eprintln!("ECDSA P-256/Sign"); + + // d = 42, well within [1, n-1] -- same fixed-key trick the crate's own tests use, so keygen's + // own stack cost is not folded into this measurement. + let mut bytes = [0u8; keys_p256::SK_LEN]; + bytes[keys_p256::SK_LEN - 1] = 0x2A; + let sk = keys_p256::ECDSAP256PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_p256_sign_deterministic() { + eprintln!("ECDetDSA P-256/Sign"); + + // d = 42, well within [1, n-1] -- same fixed-key trick the crate's own tests use, so keygen's + // own stack cost is not folded into this measurement. + let mut bytes = [0u8; keys_p256::SK_LEN]; + bytes[keys_p256::SK_LEN - 1] = 0x2A; + let sk = keys_p256::ECDSAP256PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign_deterministic(MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_p256_verify() { + eprintln!("ECDSA P-256/Verify"); + + let mut bytes = [0u8; keys_p256::SK_LEN]; + bytes[keys_p256::SK_LEN - 1] = 0x2A; + let sk = keys_p256::ECDSAP256PrivateKey::from_bytes(&bytes).unwrap(); + let pk = sk.derive_pk(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + if pk.verify(MSG, None, &sig).is_ok() { + eprintln!("Verification succeeded!"); + } else { + panic!("Verification failed! -- figure that out"); + } +} + +fn bench_p384_keygen() { + eprintln!("ECDSA P-384/KeyGen"); + + let (pk, _sk) = keys_p384::keygen(&mut DefaultRNG::default()).unwrap(); + println!("{:x?}", pk.encode()); +} + +fn bench_p384_sign() { + eprintln!("ECDSA P-384/Sign"); + + let mut bytes = [0u8; keys_p384::SK_LEN]; + bytes[keys_p384::SK_LEN - 1] = 0x2A; + let sk = keys_p384::ECDSAP384PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_p384_sign_deterministic() { + eprintln!("ECDetDSA P-384/Sign"); + + let mut bytes = [0u8; keys_p384::SK_LEN]; + bytes[keys_p384::SK_LEN - 1] = 0x2A; + let sk = keys_p384::ECDSAP384PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign_deterministic(MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_p384_verify() { + eprintln!("ECDSA P-384/Verify"); + + let mut bytes = [0u8; keys_p384::SK_LEN]; + bytes[keys_p384::SK_LEN - 1] = 0x2A; + let sk = keys_p384::ECDSAP384PrivateKey::from_bytes(&bytes).unwrap(); + let pk = sk.derive_pk(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + if pk.verify(MSG, None, &sig).is_ok() { + eprintln!("Verification succeeded!"); + } else { + panic!("Verification failed! -- figure that out"); + } +} + +fn bench_p521_keygen() { + eprintln!("ECDSA P-521/KeyGen"); + + let (pk, _sk) = keys_p521::keygen(&mut DefaultRNG::default()).unwrap(); + println!("{:x?}", pk.encode()); +} + +fn bench_p521_sign() { + eprintln!("ECDSA P-521/Sign"); + + let mut bytes = [0u8; keys_p521::SK_LEN]; + bytes[keys_p521::SK_LEN - 1] = 0x2A; + let sk = keys_p521::ECDSAP521PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_p521_sign_deterministic() { + eprintln!("ECDetDSA P-521/Sign"); + + let mut bytes = [0u8; keys_p521::SK_LEN]; + bytes[keys_p521::SK_LEN - 1] = 0x2A; + let sk = keys_p521::ECDSAP521PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign_deterministic(MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_p521_verify() { + eprintln!("ECDSA P-521/Verify"); + + let mut bytes = [0u8; keys_p521::SK_LEN]; + bytes[keys_p521::SK_LEN - 1] = 0x2A; + let sk = keys_p521::ECDSAP521PrivateKey::from_bytes(&bytes).unwrap(); + let pk = sk.derive_pk(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + if pk.verify(MSG, None, &sig).is_ok() { + eprintln!("Verification succeeded!"); + } else { + panic!("Verification failed! -- figure that out"); + } +} + +fn bench_secp256k1_keygen() { + eprintln!("ECDSA secp256k1/KeyGen"); + + let (pk, _sk) = keys_p256k1::keygen(&mut DefaultRNG::default()).unwrap(); + println!("{:x?}", pk.encode()); +} + +fn bench_secp256k1_sign() { + eprintln!("ECDSA secp256k1/Sign"); + + let mut bytes = [0u8; keys_p256k1::SK_LEN]; + bytes[keys_p256k1::SK_LEN - 1] = 0x2A; + let sk = keys_p256k1::ECDSASecp256K1PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_secp256k1_sign_deterministic() { + eprintln!("ECDetDSA secp256k1/Sign"); + + let mut bytes = [0u8; keys_p256k1::SK_LEN]; + bytes[keys_p256k1::SK_LEN - 1] = 0x2A; + let sk = keys_p256k1::ECDSASecp256K1PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign_deterministic(MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_secp256k1_verify() { + eprintln!("ECDSA secp256k1/Verify"); + + let mut bytes = [0u8; keys_p256k1::SK_LEN]; + bytes[keys_p256k1::SK_LEN - 1] = 0x2A; + let sk = keys_p256k1::ECDSASecp256K1PrivateKey::from_bytes(&bytes).unwrap(); + let pk = sk.derive_pk(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + if pk.verify(MSG, None, &sig).is_ok() { + eprintln!("Verification succeeded!"); + } else { + panic!("Verification failed! -- figure that out"); + } +} + +fn bench_bp256r1_keygen() { + eprintln!("ECDSA brainpoolP256r1/KeyGen"); + + let (pk, _sk) = keys_bp256r1::keygen(&mut DefaultRNG::default()).unwrap(); + println!("{:x?}", pk.encode()); +} + +fn bench_bp256r1_sign() { + eprintln!("ECDSA brainpoolP256r1/Sign"); + + let mut bytes = [0u8; keys_bp256r1::SK_LEN]; + bytes[keys_bp256r1::SK_LEN - 1] = 0x2A; + let sk = keys_bp256r1::ECDSABp256r1PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_bp256r1_sign_deterministic() { + eprintln!("ECDetDSA brainpoolP256r1/Sign"); + + let mut bytes = [0u8; keys_bp256r1::SK_LEN]; + bytes[keys_bp256r1::SK_LEN - 1] = 0x2A; + let sk = keys_bp256r1::ECDSABp256r1PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign_deterministic(MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_bp256r1_verify() { + eprintln!("ECDSA brainpoolP256r1/Verify"); + + let mut bytes = [0u8; keys_bp256r1::SK_LEN]; + bytes[keys_bp256r1::SK_LEN - 1] = 0x2A; + let sk = keys_bp256r1::ECDSABp256r1PrivateKey::from_bytes(&bytes).unwrap(); + let pk = sk.derive_pk(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + if pk.verify(MSG, None, &sig).is_ok() { + eprintln!("Verification succeeded!"); + } else { + panic!("Verification failed! -- figure that out"); + } +} + +fn bench_bp384r1_keygen() { + eprintln!("ECDSA brainpoolP384r1/KeyGen"); + + let (pk, _sk) = keys_bp384r1::keygen(&mut DefaultRNG::default()).unwrap(); + println!("{:x?}", pk.encode()); +} + +fn bench_bp384r1_sign() { + eprintln!("ECDSA brainpoolP384r1/Sign"); + + let mut bytes = [0u8; keys_bp384r1::SK_LEN]; + bytes[keys_bp384r1::SK_LEN - 1] = 0x2A; + let sk = keys_bp384r1::ECDSABp384r1PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_bp384r1_sign_deterministic() { + eprintln!("ECDetDSA brainpoolP384r1/Sign"); + + let mut bytes = [0u8; keys_bp384r1::SK_LEN]; + bytes[keys_bp384r1::SK_LEN - 1] = 0x2A; + let sk = keys_bp384r1::ECDSABp384r1PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign_deterministic(MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_bp384r1_verify() { + eprintln!("ECDSA brainpoolP384r1/Verify"); + + let mut bytes = [0u8; keys_bp384r1::SK_LEN]; + bytes[keys_bp384r1::SK_LEN - 1] = 0x2A; + let sk = keys_bp384r1::ECDSABp384r1PrivateKey::from_bytes(&bytes).unwrap(); + let pk = sk.derive_pk(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + if pk.verify(MSG, None, &sig).is_ok() { + eprintln!("Verification succeeded!"); + } else { + panic!("Verification failed! -- figure that out"); + } +} + +fn bench_bp512r1_keygen() { + eprintln!("ECDSA brainpoolP512r1/KeyGen"); + + let (pk, _sk) = keys_bp512r1::keygen(&mut DefaultRNG::default()).unwrap(); + println!("{:x?}", pk.encode()); +} + +fn bench_bp512r1_sign() { + eprintln!("ECDSA brainpoolP512r1/Sign"); + + let mut bytes = [0u8; keys_bp512r1::SK_LEN]; + bytes[keys_bp512r1::SK_LEN - 1] = 0x2A; + let sk = keys_bp512r1::ECDSABp512r1PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_bp512r1_sign_deterministic() { + eprintln!("ECDetDSA brainpoolP512r1/Sign"); + + let mut bytes = [0u8; keys_bp512r1::SK_LEN]; + bytes[keys_bp512r1::SK_LEN - 1] = 0x2A; + let sk = keys_bp512r1::ECDSABp512r1PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign_deterministic(MSG, None).unwrap(); + println!("{:x?}", sig); +} + +fn bench_bp512r1_verify() { + eprintln!("ECDSA brainpoolP512r1/Verify"); + + let mut bytes = [0u8; keys_bp512r1::SK_LEN]; + bytes[keys_bp512r1::SK_LEN - 1] = 0x2A; + let sk = keys_bp512r1::ECDSABp512r1PrivateKey::from_bytes(&bytes).unwrap(); + let pk = sk.derive_pk(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, None).unwrap(); + + if pk.verify(MSG, None, &sig).is_ok() { + eprintln!("Verification succeeded!"); + } else { + panic!("Verification failed! -- figure that out"); + } +} + +fn main() { + print_struct_sizes() + // bench_do_nothing() + // bench_p256_keygen() + // bench_p256_sign() + // bench_p256_sign_deterministic() + // bench_p256_verify() + // bench_p384_keygen() + // bench_p384_sign() + // bench_p384_sign_deterministic() + // bench_p384_verify() + // bench_p521_keygen() + // bench_p521_sign() + // bench_p521_sign_deterministic() + // bench_p521_verify() + // bench_secp256k1_keygen() + // bench_secp256k1_sign() + // bench_secp256k1_sign_deterministic() + // bench_secp256k1_verify() + // bench_bp256r1_keygen() + // bench_bp256r1_sign() + // bench_bp256r1_sign_deterministic() + // bench_bp256r1_verify() + // bench_bp384r1_keygen() + // bench_bp384r1_sign() + // bench_bp384r1_sign_deterministic() + // bench_bp384r1_verify() + // bench_bp512r1_keygen() + // bench_bp512r1_sign() + // bench_bp512r1_sign_deterministic() + // bench_bp512r1_verify() +} diff --git a/mem_usage_benches/src/bench_rsa_mem_usage.rs b/mem_usage_benches/src/bench_rsa_mem_usage.rs new file mode 100644 index 000000000..33dc730b6 --- /dev/null +++ b/mem_usage_benches/src/bench_rsa_mem_usage.rs @@ -0,0 +1,420 @@ +//! 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_rsa_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_rsa_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ``` +//! +//! Make sure you build in release mode! +//! +//! The code is using print!() to force the compiler not to optimize away the actual code. +//! It is printing important outputs for benchmarking to stderr so that the rest can be mapped to /dev/null +//! (this is because /usr/bin/time prints useful outputs to stderr as well) +//! +//! Only one representative (scheme, hash) is benched per modulus size (PKCS#1 v1.5/SHA-256): peak +//! stack usage here is dominated by the const-generic-width RSASP1/RSAVP1 modular exponentiation +//! (`L`, this modulus size's own limb count), not by which hash fed into it -- the +//! `crypto/rsa/benches/rsa_*_benches.rs` criterion benches are where per-hash/per-scheme +//! *time* differences are measured instead. +//! +//! Main is at the bottom, controls which this was actually run. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use bouncycastle::core::key_material::KeyMaterialTrait; +use bouncycastle::core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle::core::traits::{ + KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SignaturePrivateKey, + SignaturePublicKey, +}; +use bouncycastle::rng::DefaultRNG; +use bouncycastle::rsa::{rsa_1024, rsa_1536, rsa_2048, rsa_3072, rsa_4096, rsa_8192}; + +const MSG: &[u8] = b"peak stack usage of RSA sign/verify, held constant across every modulus size"; + +/// This prints the on-disk encoded length and the in-memory struct size of each modulus size's +/// private/public keys, and each modulus size's signature length. This is the source for the +/// table in `bouncycastle_rsa`'s own crate docs' `# Memory Usage` section. +fn print_key_sizes() { + use core::mem::size_of; + + println!("\nRSA-1024 (verification only)"); + println!("public key on disk (n || e): {} bytes", 132); + println!("size_of: {} bytes", size_of::()); + println!("signature: {} bytes", 128); + + println!("\nRSA-1536 (verification only)"); + println!("public key on disk (n || e): {} bytes", 196); + println!("size_of: {} bytes", size_of::()); + println!("signature: {} bytes", 192); + + println!("\nRSA-2048"); + println!("private key on disk (p||q||dP||dQ||qInv): {} bytes", 640); + println!("size_of: {} bytes", size_of::()); + println!("public key on disk (n || e): {} bytes", 260); + println!("size_of: {} bytes", size_of::()); + println!( + "size_of: {} bytes", + size_of::() + ); + println!("size_of: {} bytes", size_of::()); + println!( + "RSASVE ciphertext / shared secret: {} / {} bytes", + rsa_2048::CT_LEN, + rsa_2048::SS_LEN + ); + println!("signature: {} bytes", 256); + + println!("\nRSA-3072"); + println!("private key on disk (p||q||dP||dQ||qInv): {} bytes", 960); + println!("size_of: {} bytes", size_of::()); + println!("public key on disk (n || e): {} bytes", 388); + println!("size_of: {} bytes", size_of::()); + println!( + "size_of: {} bytes", + size_of::() + ); + println!("size_of: {} bytes", size_of::()); + println!( + "RSASVE ciphertext / shared secret: {} / {} bytes", + rsa_3072::CT_LEN, + rsa_3072::SS_LEN + ); + println!("signature: {} bytes", 384); + + println!("\nRSA-4096"); + println!("private key on disk (p||q||dP||dQ||qInv): {} bytes", 1280); + println!("size_of: {} bytes", size_of::()); + println!("public key on disk (n || e): {} bytes", 516); + println!("size_of: {} bytes", size_of::()); + println!( + "size_of: {} bytes", + size_of::() + ); + println!("size_of: {} bytes", size_of::()); + println!( + "RSASVE ciphertext / shared secret: {} / {} bytes", + rsa_4096::CT_LEN, + rsa_4096::SS_LEN + ); + println!("signature: {} bytes", 512); + + println!("\nRSA-8192"); + println!("private key on disk (p||q||dP||dQ||qInv): {} bytes", 2560); + println!("size_of: {} bytes", size_of::()); + println!("public key on disk (n || e): {} bytes", 1028); + println!("size_of: {} bytes", size_of::()); + println!( + "size_of: {} bytes", + size_of::() + ); + println!("size_of: {} bytes", size_of::()); + println!( + "RSASVE ciphertext / shared secret: {} / {} bytes", + rsa_8192::CT_LEN, + rsa_8192::SS_LEN + ); + println!("signature: {} bytes", 1024); +} + +/// This exists that /usr/bin/time can be used to measure the base memory footprint of the cargo bench harness +fn bench_do_nothing() { + eprintln!("DoNothing"); + + print!("{}", 1 + 1); +} + +fn bench_1024_verify() { + eprintln!("RSA-1024/Verify"); + + let n: [u64; 16] = [ + 0xd00343468eaacfbf, 0xb7c7044cc202dcca, 0x9686f30f478db649, 0x5179b54951fff6aa, + 0xbabb14f550d5d0dd, 0x5405db7c5c8f4cf6, 0x9816e2eda41fd7b9, 0xb31b6abd805bace9, + 0xb909dd0f4c6014f2, 0x9c8a5810b6d05990, 0x40760d1f23fe9250, 0x90adb011a919575a, + 0x45e48572113cab28, 0xcb9ca9ec12000fc8, 0x91b4fcaf62a14595, 0xac9048a7a4f560af, + ]; + let pk = rsa_1024::RSA1024PublicKey::new(&n, 0x10001).unwrap(); + let mut sig = [0u8; 128]; + sig.copy_from_slice( + &bouncycastle::hex::decode( + "41339884a9b3940e8488d666bb158063c6a2a2717cae7f564834a876fcbf7098ecf3acbfabf37d38a8e6127b1e313744f1f896e165efdaea0b2e7673867842b9e94db0868ed9a92bcdcb370a4e20ff275c82595e4400a8b9e9f12482f014846b48216f321266ae6ae6338dbcdc41b711e483e6e3e728772e7f9f5ef95c30196b", + ) + .unwrap(), + ); + let msg = bouncycastle::hex::decode("0000000000000000000000000000000000000000").unwrap(); + + if VerifyingKey::::verify(&pk, &msg, None, &sig) + .is_ok() + { + eprintln!("Verification succeeded!"); + } else { + panic!("Verification failed! -- figure that out"); + } +} + +fn genuine_2048_key() -> rsa_2048::RSA2048PrivateKey { + let p: [u64; 16] = [ + 0x0ea36cfb3a5b18f1, 0x48a6e65332119129, 0x110ad9e7b48a1c93, 0x569156b90113e2e9, + 0xe79813a575cfad9c, 0x69d659d143ec6f17, 0xe81e6bab5ddaa783, 0xbff1c5b80a69f788, + 0x978f6c35814f50ee, 0xe6a289ad4cfbf78f, 0x34d5681e5809d415, 0xbb028bda42eeb5d2, + 0x41c56e4de086b0d5, 0x58b8d1e24f3b55d0, 0xfb5248247d98cb7d, 0xdc431050f782e894, + ]; + let q: [u64; 16] = [ + 0x669f140cfbc20f25, 0xb97bb03677207d95, 0xfd4e06f3ed7299d4, 0x160f90536abc9492, + 0xf5b131f39098f7bc, 0xae8d72c57088d7ab, 0x89b94fbde542aba9, 0x3d3f9880ec47d5e0, + 0x1378a6868af3b7a0, 0x5544070beb057c94, 0x16611debc472fac4, 0xe500ffb79f5b8868, + 0x308a5e32196603b2, 0xea5fb19eb4eabc38, 0x122273ae3222b598, 0xbd1a81e7977f9898, + ]; + let d_p: [u64; 16] = [ + 0x209f33f09515d7c1, 0xb4a9b37656917205, 0x276933bb07e4efb9, 0x8c14019808e00414, + 0x289f96da220711e5, 0xfbbd2923d31532fe, 0xc06b414e61c0e1e7, 0x4c23c4588488961d, + 0x4dc48ae34514759c, 0x9c786961ae3e2c35, 0x497e8d9c650688e0, 0x18bf08472612dbe5, + 0x8885fb161870ee12, 0xf21d7c1479d99d47, 0x9121d91952ffd1c7, 0xa94b528b28f29159, + ]; + let d_q: [u64; 16] = [ + 0xf7597ffb68011d8d, 0x7b3cc538c4bab8c9, 0xa8fa480a81a925af, 0x6d6ede7251a383bf, + 0x8a63f788ce3a0f85, 0x0b920502eb478bc9, 0x7e37e755edfe70d9, 0x9cf9948422a16555, + 0x0d6d9ea1f2ef71fd, 0xf7efa32ea0cb6e00, 0x0629b114ca7f780f, 0xcf51176359654348, + 0x540cdcbd4ad35435, 0x31c02ff1a2bc437c, 0xff2503df78bafed5, 0x3af0e72a933aef09, + ]; + let q_inv: [u64; 16] = [ + 0x552fe4bfce945f7b, 0x67e50c999c67247b, 0xfb54ef17be3b2853, 0x241f5921b5ad3983, + 0x02de5eccd143cf31, 0x74e45f6fcc60f216, 0xafa5428a74f12708, 0x88d42294b6a2759b, + 0xe923e1097c0c562f, 0xc968b48a91c38b5b, 0x933e85179c0320b0, 0x7993d0445f758d51, + 0x9bfc042ee0924b1b, 0x41f956d90fa8a793, 0xee7a87b6483a66ee, 0x2640fbfbcfefb163, + ]; + rsa_2048::RSA2048PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv).unwrap() +} + +/// FIPS 186-5 A.1.3 key generation: the candidate buffers, the Miller-Rabin Montgomery context +/// and `mod_pow`'s ladder at the CRT-prime width, plus the `lcm(p - 1, q - 1)`/`d` bound check at +/// the modulus width. +fn bench_2048_keygen() { + eprintln!("RSA-2048/Keygen"); + let (pk, sk) = rsa_2048::keygen(&mut DefaultRNG::default()).unwrap(); + println!("{:x?}", pk.encode()); + println!("{}", sk.encode().len()); +} + +fn bench_2048_sign() { + eprintln!("RSA-2048/Sign"); + let sk = genuine_2048_key(); + let sig = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + println!("{:x?}", sig); +} + +fn bench_2048_verify() { + eprintln!("RSA-2048/Verify"); + let sk = genuine_2048_key(); + let pk = rsa_2048::RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + let sig = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + + if VerifyingKey::::verify(&pk, MSG, None, &sig).is_ok() + { + eprintln!("Verification succeeded!"); + } else { + panic!("Verification failed! -- figure that out"); + } +} + +fn genuine_8192_key() -> rsa_8192::RSA8192PrivateKey { + let p: [u64; 64] = [ + 0xa9a87892c469b1e5, 0x038c3f4efcb198ac, 0xed4f14df1b8a1e1f, 0xef2330d78ee73020, + 0xd3093ddaf276904c, 0x94a438d69aeaff23, 0x48dbb3d700ad58ed, 0xbbd763b5346ff916, + 0xea402fb475938e3e, 0xcaed4c45c543aff3, 0xc0954c4255b90f57, 0x2fe6242da64a5099, + 0x3fd54a6b6175342c, 0xdf8dd0187588abbd, 0x4abc11d33376e220, 0x90d3482c7fc1397d, + 0xde925fb5a2cb1e7e, 0x062e99dfbd3c790b, 0x243ef703c0912a1e, 0x91ee31a02ef1a02e, + 0x13b19d3960842877, 0xc6e8036e26c4a339, 0xf2606f45a73314c9, 0x5dfe4c6dd0aa04ee, + 0x1385bfde55204d94, 0x454123829d28200d, 0x18a3d86f516f2a50, 0xcbe5e841cd2fdb17, + 0xc86825aae3031a2c, 0xf4726a6681852b6e, 0x3f16b45a496eee29, 0xd5c4b2319abff492, + 0x71077736745aee55, 0x72586dba7b765bbe, 0x681813f6851efdce, 0x196c13fa6d6781d2, + 0x4712ba3eb0734e43, 0xde2a9a8da6da8340, 0xc2f93a2af8ffbaaa, 0x4db552ab82a577ae, + 0x07c075eb13064f67, 0x818a7d0b31680682, 0xa58320f732d69460, 0xa5922479f4aaf594, + 0x4d545baa4cd73f39, 0xa20c87a2660869c4, 0x8350211511fe7d7a, 0x50174a372234ba89, + 0x6eefd36b20bd1ce7, 0x07d124cb61d25c8b, 0x7356e6380041dcd5, 0x6a7c98caab76211a, + 0x61ff7eaac65f815d, 0x1167dc0b28f418b3, 0x9fc8bf056efe8677, 0xf5ce4481e1670e10, + 0x6ae488da222292de, 0xcc786e194ea84d2a, 0x41b134cb6a8ae4f5, 0x870ff94b064466b7, + 0xc82ad608c14a9385, 0x5338550ac9d3f96d, 0xdce87e0d41f1ba9d, 0xef9beedb39920589, + ]; + let q: [u64; 64] = [ + 0x08798e2dd82b7d9f, 0x30208e668897171d, 0x8f73c28d70144ec6, 0x66f64ce7d2402ae0, + 0xdbc26c601d5cc3a3, 0x657176891201beee, 0x692812d7b9b796c5, 0xae27fa9062406a23, + 0x7b4e6ee5b4d2d22f, 0xd846e0e8fbeb0108, 0x52ca75d6866b780b, 0x2620152d7bb490c0, + 0xdca773f826ba9118, 0xb308499a61d0a4eb, 0x44ba0f58bff07ed5, 0x268483fe5829eb5c, + 0x02e7f0c1d3ee3856, 0x76eae3a004d0f37a, 0x99d9cb3aab5b3f1f, 0x0c9f76678a44f2c4, + 0xc82ba3f75883e512, 0x57719c85e8888746, 0x63c967ca1b81cf17, 0x6f4942a86e96bb6a, + 0x614105324fdb77ae, 0xdb6a0c777f7a4bbc, 0x184bf293127472d5, 0xc26798126a967a8f, + 0x45fa10ccadd5ef6e, 0x13cacbbf5cbc2722, 0x23b4e2f2d18785fd, 0x83a4d39485ad254b, + 0x3a096f5ddd674174, 0x6df8f2ed81ae9e7e, 0x77ee2d29fb78201d, 0x4b4c10c9ac8853eb, + 0x35690c51d0ff6208, 0x8de9a271cb50aa59, 0x51b0701f8cdd1505, 0x4cb715c9b578c63b, + 0x3c9b35776d94c3c7, 0xe952d6a7c34a2abb, 0xc432f1fcc2a5105f, 0x4f7f23a62cea2f81, + 0xfa507889f33eedd9, 0x7e37e435cb776085, 0x576b2ad8a4f39820, 0x0a85d1466d55ad69, + 0xb1dee047621be714, 0x216c6b3c1c945527, 0xe6298f6e1575e39e, 0xdd16506432adc399, + 0xc1c065319c823d2e, 0xf2d6f25d6f34a582, 0x75d6801f595c50fc, 0xccfcd556c20e1130, + 0xa24e6a1396173d85, 0xe2cbc5218657c80a, 0xe7bec5bf5a944815, 0x656f5fa54bf06f6c, + 0x7b68b002b0ea62d7, 0xa2eb84753d291781, 0xdd329e1ce00c2087, 0xcdb932d13e38ee0a, + ]; + let d_p: [u64; 64] = [ + 0x848c4d06bcab4c19, 0x2fa349116ae2bfd6, 0xcf9ea12f426f7eb3, 0xfa71d15136d85dff, + 0xc3e3a026e3968f1f, 0xd50f8056de82a33e, 0x68213465c1b3b2d7, 0x426387f62ee9c62e, + 0xeb14efe18920a873, 0xf3c385a60c9dc86c, 0x294ffd8852c4d95e, 0x940d14b53927fef5, + 0x3f709e3b4c8390c3, 0x3b9737d39c42994b, 0x5c02182c4dac78fe, 0x3b6b7c01fe0a0737, + 0x7adeedf6194f6d2c, 0x43c30f449d8ae36c, 0xb88cebf30dac95d4, 0x09fc3d623b8ceb6b, + 0x5d28293b759f2b09, 0xd350dee095846993, 0x1b323dc92b794164, 0xe19d52f0e7ef34bb, + 0x5ff017da15232c02, 0x58fa8d3b24b82973, 0x1c8aed916227e53f, 0xad49cd8938c909d4, + 0xc3d98be0501534a1, 0x91cec83019bd5bb6, 0x985b814d838ffdd2, 0x11df5cb357c86888, + 0xbc0d5590336b8dcf, 0xc25fa85b1912ac66, 0x390c8752384cadef, 0x07b54e62df65825a, + 0x7f7ad57e592cfc3b, 0xed1b7f53d6fe2a09, 0x41d9e577118adb13, 0xb6706780ace45139, + 0xbfbaadef8d19443b, 0x3dc42a1dccca1d87, 0xdb8a72a8ca8d4de7, 0xe10dec18507dbc43, + 0x66343911cfeb0313, 0xd29def907cf51b72, 0x998add714f12ef6f, 0x1c394a4b8053f8ec, + 0x46e0b76d81a07852, 0x498c3f81cb9f2b8d, 0x8fd0019a024e19be, 0xf2d405423b1d94e4, + 0x29ce52d9c49ff544, 0xd72643a03a2f9053, 0x1be76a355cfa02d2, 0x8db921cec169edf6, + 0x728f91a0f7d9dc15, 0xe34e3a4f3d41d26d, 0xab0a691f05332eb8, 0xb6241cc75fd5ee1b, + 0x5b8582afd5c1acda, 0xe405bceba411da3e, 0xd849aacac396ef0e, 0x57b5a6ebccd92cf9, + ]; + let d_q: [u64; 64] = [ + 0x77a312e8aa49ed35, 0x3bdc007b0396dd44, 0x187af52b1d2d6e88, 0x7d84fae3e1b134fa, + 0xb1af50faa281bf7d, 0xf868401279f57864, 0x1dba1e456f88eb01, 0xe2ee28d0f43ba792, + 0x3450781c72a68626, 0x30757ab4889c0c41, 0x7f34c53326389381, 0xa28e969fda4bc5e4, + 0x0ed664e6b7a53062, 0xfc9ad2c9c2bd3cf2, 0x1a7bb5627d58e8ba, 0x68b8c4da5a5a5104, + 0x5a0e586f0ebe7967, 0x2dcff7ca08c9b702, 0x89a724facd778478, 0x027fe07b2ad90a40, + 0xd2f67b0b487433d5, 0x28d451f2f0eaa3ad, 0xee8d5d55700c24ca, 0x584202d4e805a701, + 0xa1c40f0ed77d96a5, 0xc4055e480f1674ba, 0xf9fb2aeb561bf104, 0x6b846fe9ef936161, + 0x128295b5f01cf2ad, 0x4aaff225e483b5dd, 0x7cba43b61174a21c, 0xf68f9c7b4474bcbd, + 0xc8979d3dd627346e, 0x9c2b543e42af83ed, 0x221d3779bb596a50, 0xac4d7c5545e7c88b, + 0xa44c83a4cb2ffe15, 0x414d71c3a67b3563, 0xe22dd5c34f66bf5e, 0x6a9cd6374f4513e7, + 0x27765c4e3c8ee4d5, 0xf632603a9e123d3f, 0x748b0b2d39fcb6bd, 0x0bde2fc8edf891e6, + 0xded2b9cf10705222, 0xe5ca5d358eb8067a, 0x23368f3218b1bd0c, 0xe41d6519183900d0, + 0x36d90382237f0987, 0x858624f4663a543a, 0x2309265e924e5f4c, 0xfc548ae0263785e8, + 0xe010d5d5e76f3406, 0x963cc28c4228ff36, 0x4245a573c17ac537, 0x7e9ab38877e4d606, + 0xc69b8cfcee4fba17, 0xe127881203d02766, 0xf3b33254ff10a7b7, 0x4df7b087512a53dd, + 0x28ffecc1c7a3ee5d, 0x4c693f99aa4b22b7, 0x498cdaef4c3d1223, 0x78c24ba834042b07, + ]; + let q_inv: [u64; 64] = [ + 0xd87df1de9c759f36, 0x3ee1b8b22c8eab2d, 0xd6098595c2696717, 0xa922e4722b8304de, + 0x424bec1c55c1db54, 0x918f2f8bf0ac261a, 0x1e190b2089185564, 0xc0dedbfc6ee606bb, + 0x1d1d701caca565c9, 0x2d735f7a1c5b9d67, 0x8339684f656205db, 0xc2dc06896727d9ce, + 0x6c8d6fe334141d31, 0x5245e157c87cd8b4, 0x4975b68eb2eaad9a, 0xfbc3389ffb0e788d, + 0x5e9a45250d135847, 0x676e7e6a128cfa88, 0x74341d8b28dcbcb8, 0x8f7d07f37a0490e0, + 0x83a0eae9ecc99a0e, 0xa8188892180dc052, 0x3cd79e1eea7809cb, 0x3bcc1d769d917b24, + 0x1d8dc8edc7268e8d, 0x3ce2707055f522ca, 0x163ead0ca2eb5fee, 0xf147fb695aa70022, + 0xab406a6c89161f7d, 0x8fbbfe117a95f46e, 0xa16d4e65396165bd, 0xb54f52ab1bb357e5, + 0x67cdec7b6a349ed9, 0x303875e0d97cf3fa, 0x0af9378ecb114fd8, 0x4ada6f45b9d6b1df, + 0x9ef2cf55de53d0ed, 0x33e4c2070b165b52, 0x3feddf0f7a1158c0, 0x91463a2b1952e83a, + 0x65a9db73de55ef6b, 0x8e18c716a69a9a6a, 0x0ca44356ca6e2898, 0x7df214b19f2114f1, + 0x9af2aee9d5cf1077, 0x22fc8b019fd3d0ff, 0x50c6eead29ab18cd, 0xa7e6621f8eae45fe, + 0x588bde2d99b8692d, 0x339be0a07b4f0a7b, 0xcbdbdcc8717105d2, 0x78bbd3e3392a503e, + 0xb868f9db1cf14f74, 0x52f3dc7f028c9f7e, 0xa3629e034b453834, 0x52bab134f3ae91a2, + 0xd1aac2fe108f98c0, 0x1e799537a42b3060, 0xdbcce6fc287e449b, 0x1985969e5afbc524, + 0x61ba61f820f83597, 0x65c8523f0767bccc, 0xde04e1644a214f9a, 0x9748f8d853be7131, + ]; + rsa_8192::RSA8192PrivateKey::from_crt_components(&p, &q, &d_p, &d_q, &q_inv).unwrap() +} + +fn bench_8192_sign() { + eprintln!("RSA-8192/Sign"); + let sk = genuine_8192_key(); + let sig = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + println!("{:x?}", sig); +} + +fn bench_8192_verify() { + eprintln!("RSA-8192/Verify"); + let sk = genuine_8192_key(); + let pk = rsa_8192::RSA8192PublicKey::new(sk.n(), 0x10001).unwrap(); + let sig = + DeterministicSigningKey::::sign_deterministic( + &sk, MSG, None, + ) + .unwrap(); + + if VerifyingKey::::verify(&pk, MSG, None, &sig) + .is_ok() + { + eprintln!("Verification succeeded!"); + } else { + panic!("Verification failed! -- figure that out"); + } +} + +/// RSASVE.GENERATE (SP 800-56B Rev. 2 §7.2.1.2): the `nLen`-byte `Z`/`z` buffers and one +/// public-exponent `mod_pow` at the modulus width. +fn bench_2048_rsasve_encaps() { + eprintln!("RSA-2048/RSASVE encaps"); + let sk = genuine_2048_key(); + let pk = rsa_2048::RSA2048PublicKey::new(sk.n(), 0x10001).unwrap(); + let pk = rsa_2048::RSA2048KEMPublicKey::from_bytes(&pk.encode()).unwrap(); + let (ss, ct) = rsa_2048::RSASVE::encaps(&pk).unwrap(); + println!("{:x?}", ct); + println!("{}", ss.key_len()); +} + +/// RSASVE.RECOVER (SP 800-56B Rev. 2 §7.2.1.3): the same CRT exponentiation as signing, plus +/// the `nLen`-byte `Z` buffer. +fn bench_2048_rsasve_decaps() { + eprintln!("RSA-2048/RSASVE decaps"); + let sk = rsa_2048::RSA2048KEMPrivateKey::from_bytes(&genuine_2048_key().encode()).unwrap(); + let mut ct = [0x5Au8; rsa_2048::CT_LEN]; + ct[0] = 0x01; // well inside (1, n - 1) + let ss = rsa_2048::RSASVE::decaps(&sk, &ct).unwrap(); + println!("{}", ss.key_len()); +} + +/// RSASVE.GENERATE (SP 800-56B Rev. 2 §7.2.1.2): the `nLen`-byte `Z`/`z` buffers and one +/// public-exponent `mod_pow` at the modulus width. +fn bench_8192_rsasve_encaps() { + eprintln!("RSA-8192/RSASVE encaps"); + let sk = genuine_8192_key(); + let pk = rsa_8192::RSA8192PublicKey::new(sk.n(), 0x10001).unwrap(); + let pk = rsa_8192::RSA8192KEMPublicKey::from_bytes(&pk.encode()).unwrap(); + let (ss, ct) = rsa_8192::RSASVE::encaps(&pk).unwrap(); + println!("{:x?}", ct); + println!("{}", ss.key_len()); +} + +/// RSASVE.RECOVER (SP 800-56B Rev. 2 §7.2.1.3): the same CRT exponentiation as signing, plus +/// the `nLen`-byte `Z` buffer. +fn bench_8192_rsasve_decaps() { + eprintln!("RSA-8192/RSASVE decaps"); + let sk = rsa_8192::RSA8192KEMPrivateKey::from_bytes(&genuine_8192_key().encode()).unwrap(); + let mut ct = [0x5Au8; rsa_8192::CT_LEN]; + ct[0] = 0x01; // well inside (1, n - 1) + let ss = rsa_8192::RSASVE::decaps(&sk, &ct).unwrap(); + println!("{}", ss.key_len()); +} + +fn main() { + print_key_sizes() + // bench_do_nothing() + // bench_1024_verify() + // bench_2048_keygen() + // bench_2048_sign() + // bench_2048_verify() + // bench_8192_sign() + // bench_8192_verify() + // bench_2048_rsasve_encaps() + // bench_2048_rsasve_decaps() + // bench_8192_rsasve_encaps() + // bench_8192_rsasve_decaps() +} diff --git a/mem_usage_benches/src/bench_sm2_mem_usage.rs b/mem_usage_benches/src/bench_sm2_mem_usage.rs new file mode 100644 index 000000000..1a6182f96 --- /dev/null +++ b/mem_usage_benches/src/bench_sm2_mem_usage.rs @@ -0,0 +1,102 @@ +//! 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_sm2_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_sm2_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ``` +//! +//! Make sure you build in release mode! +//! +//! The code is using print!() to force the compiler not to optimize away the actual code. +//! It is printing important outputs for benchmarking to stderr so that the rest can be mapped to /dev/null +//! (this is because /usr/bin/time prints useful outputs to stderr as well) +//! +//! Main is at the bottom, controls which this was actually run. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use bouncycastle::core::signature_key::{DeterministicSigningKey, SigningKey, VerifyingKey}; +use bouncycastle::core::traits::{SignaturePrivateKey, SignaturePublicKey}; +use bouncycastle::core::traits::{SignatureSigner, SignatureVerifier}; +use bouncycastle::rng::DefaultRNG; +use bouncycastle::sm2::keys::{PK_LEN, SK_LEN, SM2PrivateKey, SM2PublicKey, keygen}; +use bouncycastle::sm2::sm2::SM2; + +/// A fixed message, reused across the sign/verify benches. +const MSG: &[u8] = b"peak stack usage of SM2 sign/verify, held constant across runs"; + +/// SM2 requires `ctx` to carry the signer's identity `IDA`; this is a fixed placeholder, not a +/// crate-provided default (the crate has none -- see `bouncycastle_sm2::sm2`'s docs). +const ID: &[u8] = b"1234567812345678"; + +/// This prints the in-memory size of the public and private key structs, plus their on-disk +/// encoded length. +fn print_struct_sizes() { + use core::mem::size_of; + + println!("\nSM2"); + println!("size_of: {}", size_of::()); + println!("PK_LEN (on disk): {}", PK_LEN); + println!("size_of: {}", size_of::()); + println!("SK_LEN (on disk): {}", SK_LEN); +} + +/// This exists that /usr/bin/time can be used to measure the base memory footprint of the cargo bench harness +fn bench_do_nothing() { + eprintln!("DoNothing"); + + print!("{}", 1 + 1); +} + +fn bench_sm2_keygen() { + eprintln!("SM2/KeyGen"); + + let (pk, _sk) = keygen(&mut DefaultRNG::default()).unwrap(); + println!("{:x?}", pk.encode()); +} + +fn bench_sm2_sign() { + eprintln!("SM2/Sign"); + + // dA = 42, well within [1, n-1] -- same fixed-key trick the crate's own tests use, so + // keygen's own stack cost is not folded into this measurement. + let mut bytes = [0u8; SK_LEN]; + bytes[SK_LEN - 1] = 0x2A; + let sk = SM2PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = sk.sign(&mut DefaultRNG::default(), MSG, Some(ID)).unwrap(); + println!("{:x?}", sig); +} + +fn bench_sm2_verify() { + eprintln!("SM2/Verify"); + + let mut bytes = [0u8; SK_LEN]; + bytes[SK_LEN - 1] = 0x2A; + let sk = SM2PrivateKey::from_bytes(&bytes).unwrap(); + let pk = sk.derive_pk(); + let sig = sk.sign(&mut DefaultRNG::default(), MSG, Some(ID)).unwrap(); + + if pk.verify(MSG, Some(ID), &sig).is_ok() { + eprintln!("Verification succeeded!"); + } else { + panic!("Verification failed! -- figure that out"); + } +} + +fn main() { + print_struct_sizes() + // bench_do_nothing() + // bench_sm2_keygen() + // bench_sm2_sign() + // bench_sm2_verify() +} diff --git a/mem_usage_benches/src/lib.rs b/mem_usage_benches/src/lib.rs index 231a1b730..774c5405c 100644 --- a/mem_usage_benches/src/lib.rs +++ b/mem_usage_benches/src/lib.rs @@ -1,8 +1,11 @@ mod bench_aes_mem_usage; mod bench_ascon_mem_usage; mod bench_ccm_mem_usage; +mod bench_ecdsa_mem_usage; mod bench_mldsa_mem_usage; mod bench_mlkem_mem_usage; +mod bench_rsa_mem_usage; mod bench_sha2_mem_usage; mod bench_sha3_mem_usage; +mod bench_sm2_mem_usage; mod bench_sm3_mem_usage; diff --git a/src/lib.rs b/src/lib.rs index ae250f332..0ce550d9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub use bouncycastle_ascon as ascon; pub use bouncycastle_base64 as base64; pub use bouncycastle_cipher as cipher; pub use bouncycastle_core as core; +pub use bouncycastle_ecdsa as ecdsa; pub use bouncycastle_factory as factory; pub use bouncycastle_hex as hex; pub use bouncycastle_hkdf as hkdf; @@ -12,6 +13,8 @@ pub use bouncycastle_mldsa_lowmemory as mldsa_lowmemory; pub use bouncycastle_mlkem as mlkem; pub use bouncycastle_mlkem_lowmemory as mlkem_lowmemory; pub use bouncycastle_rng as rng; +pub use bouncycastle_rsa as rsa; pub use bouncycastle_sha2 as sha2; pub use bouncycastle_sha3 as sha3; +pub use bouncycastle_sm2 as sm2; pub use bouncycastle_sm3 as sm3;