From e168720cb895bb7e7d52d168933565beb4d88114 Mon Sep 17 00:00:00 2001 From: zz-sol Date: Mon, 26 Jan 2026 10:01:58 -0500 Subject: [PATCH 1/8] implement falcon signature --- Cargo.lock | 161 +++++++++++++- Cargo.toml | 4 +- falcon-signature/Cargo.toml | 29 +++ falcon-signature/src/constants.rs | 28 +++ falcon-signature/src/error.rs | 50 +++++ falcon-signature/src/instruction.rs | 113 ++++++++++ falcon-signature/src/lib.rs | 110 ++++++++++ falcon-signature/src/offsets.rs | 50 +++++ falcon-signature/src/public_key.rs | 83 ++++++++ falcon-signature/src/secret_key.rs | 51 +++++ falcon-signature/src/signature.rs | 85 ++++++++ falcon-signature/src/tests.rs | 312 ++++++++++++++++++++++++++++ message/src/sanitized.rs | 68 +++++- program/src/falcon512_program.rs | 4 + program/src/lib.rs | 1 + sdk-ids/src/lib.rs | 7 + 16 files changed, 1153 insertions(+), 3 deletions(-) create mode 100644 falcon-signature/Cargo.toml create mode 100644 falcon-signature/src/constants.rs create mode 100644 falcon-signature/src/error.rs create mode 100644 falcon-signature/src/instruction.rs create mode 100644 falcon-signature/src/lib.rs create mode 100644 falcon-signature/src/offsets.rs create mode 100644 falcon-signature/src/public_key.rs create mode 100644 falcon-signature/src/secret_key.rs create mode 100644 falcon-signature/src/signature.rs create mode 100644 falcon-signature/src/tests.rs create mode 100644 program/src/falcon512_program.rs diff --git a/Cargo.lock b/Cargo.lock index 15cb4e9b6..1cc653945 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -419,6 +419,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.9.4", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.111", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -580,6 +600,15 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "build-deps" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f14468960818ce4f3e3553c32d524446687884f8e7af5d3e252331d8a87e43" +dependencies = [ + "glob", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -649,6 +678,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -708,6 +746,17 @@ dependencies = [ "half", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.39" @@ -733,6 +782,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "console" version = "0.15.10" @@ -901,6 +959,22 @@ dependencies = [ "typenum", ] +[[package]] +name = "cstr_core" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd98742e4fdca832d40cab219dc2e3048de17d873248f83f17df47c1bea70956" +dependencies = [ + "cty", + "memchr", +] + +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1888,6 +1962,16 @@ version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "light-poseidon" version = "0.2.0" @@ -1970,6 +2054,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.3" @@ -1990,6 +2080,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -2135,6 +2235,30 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "oqs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48caac02cf42ba00b865a747e332828a75341d97ae35ad1ae9785e56de212e78" +dependencies = [ + "cstr_core", + "libc", + "oqs-sys", +] + +[[package]] +name = "oqs-sys" +version = "0.11.0+liboqs-0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6d66ee528a895ce5cc08851698d109c5d7ee5d7a0b3b40d61550eda91e414f" +dependencies = [ + "bindgen", + "build-deps", + "cmake", + "libc", + "pkg-config", +] + [[package]] name = "pairing" version = "0.23.0" @@ -2269,6 +2393,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -2576,6 +2710,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3285,6 +3425,19 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "solana-falcon-signature" +version = "0.1.0" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "oqs", + "rand 0.9.2", + "solana-instruction", + "solana-sdk-ids", + "thiserror 2.0.17", +] + [[package]] name = "solana-feature-gate-interface" version = "3.0.0" @@ -4609,7 +4762,7 @@ dependencies = [ "once_cell", "pbkdf2 0.12.2", "rand 0.8.5", - "rustc-hash", + "rustc-hash 1.1.0", "sha2", "thiserror 1.0.69", "unicode-normalization", @@ -5020,6 +5173,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index f9331760e..6db778204 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,8 @@ members = [ "epoch-rewards-hasher", "epoch-schedule", "epoch-stake", - "example-mocks", + "example-mocks", + "falcon-signature", "feature-gate-interface", "fee-calculator", "fee-structure", @@ -195,6 +196,7 @@ num-derive = "0.4" num-traits = { version = "0.2.18", default-features = false } num_enum = "0.7.3" openssl = "0.10.72" +oqs = { version = "0.11", default-features = false, features = ["std", "falcon", "vendored"] } pairing = "0.23.0" parking_lot = "0.12" pbkdf2 = { version = "0.11.0", default-features = false } diff --git a/falcon-signature/Cargo.toml b/falcon-signature/Cargo.toml new file mode 100644 index 000000000..74870bb75 --- /dev/null +++ b/falcon-signature/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "solana-falcon-signature" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +license.workspace = true +edition.workspace = true +description = "Falcon-512 post-quantum signature support for Solana SDK" + +[features] +default = ["std"] +std = [] + +[dependencies] +bytemuck = { workspace = true } +bytemuck_derive = { workspace = true } +solana-instruction = { workspace = true, features = ["std"] } +solana-sdk-ids = { workspace = true } +thiserror = { workspace = true } + +[target.'cfg(not(target_os = "solana"))'.dependencies] +oqs = { workspace = true } + +[dev-dependencies] +rand = { workspace = true } + +[lints] +workspace = true diff --git a/falcon-signature/src/constants.rs b/falcon-signature/src/constants.rs new file mode 100644 index 000000000..f216b0f6b --- /dev/null +++ b/falcon-signature/src/constants.rs @@ -0,0 +1,28 @@ +// ============================================================================= +// Provisional Constants (per draft FIPS 206 / SIMD proposal) +// Update these when final standards are published +// ============================================================================= + +/// Size of a Falcon-512 public key in bytes (includes 1-byte header). +pub const PUBKEY_SIZE: usize = 897; + +/// Header byte for Falcon-512 public keys (0x00 + logn where logn=9). +pub const PUBKEY_HEADER: u8 = 0x09; + +/// Minimum size of a Falcon-512 signature in bytes (includes 1-byte header). +pub const MIN_SIGNATURE_SIZE: usize = 41; + +/// Maximum size of a Falcon-512 signature in bytes (includes 1-byte header). +pub const MAX_SIGNATURE_SIZE: usize = 666; + +/// Header byte for Falcon-512 signatures (0x30 + logn where logn=9). +pub const SIGNATURE_HEADER: u8 = 0x39; + +/// Size of the signature offsets structure in bytes. +pub const SIGNATURE_OFFSETS_SIZE: usize = 16; + +/// Start offset for signature offsets in instruction data. +pub const SIGNATURE_OFFSETS_START: usize = 2; + +/// Start offset for variable data in instruction data. +pub const DATA_START: usize = SIGNATURE_OFFSETS_START + SIGNATURE_OFFSETS_SIZE; diff --git a/falcon-signature/src/error.rs b/falcon-signature/src/error.rs new file mode 100644 index 000000000..431cc291f --- /dev/null +++ b/falcon-signature/src/error.rs @@ -0,0 +1,50 @@ +use crate::constants::{ + MAX_SIGNATURE_SIZE, MIN_SIGNATURE_SIZE, PUBKEY_HEADER, PUBKEY_SIZE, SIGNATURE_HEADER, +}; + +/// Errors that can occur during Falcon-512 operations. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] +pub enum FalconError { + /// Public key has invalid size. + #[cfg_attr( + feature = "std", + error("invalid public key size: expected {PUBKEY_SIZE}, got {0}") + )] + InvalidPublicKeySize(usize), + + /// Public key has invalid header byte. + #[cfg_attr( + feature = "std", + error("invalid public key header: expected {PUBKEY_HEADER:#04x}, got {0:#04x}") + )] + InvalidPublicKeyHeader(u8), + + /// Signature has invalid size. + #[cfg_attr( + feature = "std", + error( + "invalid signature size: expected {MIN_SIGNATURE_SIZE}..={MAX_SIGNATURE_SIZE}, got {0}" + ) + )] + InvalidSignatureSize(usize), + + /// Signature has invalid header byte. + #[cfg_attr( + feature = "std", + error("invalid signature header: expected {SIGNATURE_HEADER:#04x}, got {0:#04x}") + )] + InvalidSignatureHeader(u8), + + /// Signature verification failed. + #[cfg_attr(feature = "std", error("signature verification failed"))] + VerificationFailed, + + /// Key generation failed. + #[cfg_attr(feature = "std", error("key generation failed: {0}"))] + KeyGenerationFailed(&'static str), + + /// Signing operation failed. + #[cfg_attr(feature = "std", error("signing failed: {0}"))] + SigningFailed(&'static str), +} diff --git a/falcon-signature/src/instruction.rs b/falcon-signature/src/instruction.rs new file mode 100644 index 000000000..10b906632 --- /dev/null +++ b/falcon-signature/src/instruction.rs @@ -0,0 +1,113 @@ +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; +use { + crate::{ + constants::{DATA_START, PUBKEY_SIZE, SIGNATURE_OFFSETS_SIZE, SIGNATURE_OFFSETS_START}, + offsets::Falcon512SignatureOffsets, + public_key::PublicKey, + signature::Signature, + }, + bytemuck::bytes_of, + solana_instruction::Instruction, +}; + +/// Creates a Falcon-512 verification instruction from pre-computed offsets. +/// +/// This function encodes the signature offsets into instruction data, allowing +/// signatures, public keys, and messages to be located in other instructions +/// within the same transaction. +/// +/// # Arguments +/// * `offsets` - Slice of offset structures specifying where to find verification data +/// +/// # Returns +/// An instruction for the Falcon-512 signature verification program +pub fn offsets_to_falcon512_instruction(offsets: &[Falcon512SignatureOffsets]) -> Instruction { + let mut instruction_data = Vec::with_capacity( + SIGNATURE_OFFSETS_START + .saturating_add(SIGNATURE_OFFSETS_SIZE.saturating_mul(offsets.len())), + ); + + let num_signatures = offsets.len() as u8; + // Add num_signatures and padding byte for alignment + instruction_data.push(num_signatures); + instruction_data.push(0); // padding + + for offset in offsets { + instruction_data.extend_from_slice(bytes_of(offset)); + } + + Instruction { + program_id: solana_sdk_ids::falcon512_program::id(), + accounts: vec![], + data: instruction_data, + } +} + +/// Creates a Falcon-512 verification instruction with embedded signature data. +/// +/// This is a convenience function that creates a self-contained instruction +/// with the public key, signature, and message all embedded in the instruction data. +/// +/// # Arguments +/// * `message` - The message that was signed +/// * `signature` - The Falcon-512 signature (variable length, 41-666 bytes) +/// * `pubkey` - The Falcon-512 public key (897 bytes) +/// +/// # Returns +/// An instruction for the Falcon-512 signature verification program +pub fn new_falcon512_instruction_with_signature( + message: &[u8], + signature: &Signature, + pubkey: &PublicKey, +) -> Instruction { + let signature_bytes = signature.as_bytes(); + let pubkey_bytes = pubkey.as_bytes(); + + let mut instruction_data = Vec::with_capacity( + DATA_START + .saturating_add(PUBKEY_SIZE) + .saturating_add(signature_bytes.len()) + .saturating_add(message.len()), + ); + + let num_signatures: u8 = 1; + let public_key_offset = DATA_START; + let signature_offset = public_key_offset.saturating_add(PUBKEY_SIZE); + let message_offset = signature_offset.saturating_add(signature_bytes.len()); + + // Add num_signatures and padding byte for alignment + instruction_data.push(num_signatures); + instruction_data.push(0); // padding + + let offsets = Falcon512SignatureOffsets { + signature_offset: signature_offset as u16, + signature_length: signature_bytes.len() as u16, + signature_instruction_index: u16::MAX, // current instruction + public_key_offset: public_key_offset as u16, + public_key_instruction_index: u16::MAX, // current instruction + message_offset: message_offset as u16, + message_length: message.len() as u16, + message_instruction_index: u16::MAX, // current instruction + }; + + instruction_data.extend_from_slice(bytes_of(&offsets)); + + debug_assert_eq!(instruction_data.len(), public_key_offset); + + instruction_data.extend_from_slice(pubkey_bytes); + + debug_assert_eq!(instruction_data.len(), signature_offset); + + instruction_data.extend_from_slice(signature_bytes); + + debug_assert_eq!(instruction_data.len(), message_offset); + + instruction_data.extend_from_slice(message); + + Instruction { + program_id: solana_sdk_ids::falcon512_program::id(), + accounts: vec![], + data: instruction_data, + } +} diff --git a/falcon-signature/src/lib.rs b/falcon-signature/src/lib.rs new file mode 100644 index 000000000..ba229d670 --- /dev/null +++ b/falcon-signature/src/lib.rs @@ -0,0 +1,110 @@ +//! Falcon-512 post-quantum signature support for Solana SDK. +//! +//! This crate provides Falcon-512 signature operations using the `oqs` crate +//! (liboqs Rust bindings) as the cryptographic backend. Falcon-512 is a +//! lattice-based post-quantum signature scheme selected by NIST for +//! standardization (FIPS 206). +//! +//! # Overview +//! +//! Falcon-512 coexists alongside Ed25519 as an additional signature option, +//! providing post-quantum security for Solana transactions. The implementation +//! follows the SIMD-0152 precompile instruction format. +//! +//! # Key Features +//! +//! - **Post-quantum security**: Resistant to attacks from quantum computers +//! - **Variable-length signatures**: upto 666 bytes (compressed format) +//! - **897-byte public keys**: Includes FIPS 206 header byte (0x09) +//! - **SIMD-0152 compliant**: Instruction format matches other precompiles +//! +//! # Example: Signing and Verification +//! +//! ```ignore +//! use solana_falcon_signature::{SecretKey, PublicKey, Signature}; +//! +//! // Generate a new keypair +//! let secret_key = SecretKey::generate().expect("key generation failed"); +//! let public_key = secret_key.public_key(); +//! +//! // Sign a message +//! let message = b"Hello, post-quantum world!"; +//! let signature = secret_key.sign(message).expect("signing failed"); +//! +//! // Verify the signature +//! public_key.verify(message, &signature).expect("verification failed"); +//! ``` +//! +//! # Example: Creating a Verification Instruction +//! +//! ``` +//! use solana_falcon_signature::{ +//! Falcon512SignatureOffsets, offsets_to_falcon512_instruction, +//! SIGNATURE_OFFSETS_SIZE, SIGNATURE_OFFSETS_START, +//! }; +//! +//! // Create offsets for verification data in the current instruction +//! let offsets = Falcon512SignatureOffsets { +//! signature_offset: 18, +//! signature_length: 650, +//! signature_instruction_index: u16::MAX, // current instruction +//! public_key_offset: 668, +//! public_key_instruction_index: u16::MAX, +//! message_offset: 1565, +//! message_length: 100, +//! message_instruction_index: u16::MAX, +//! }; +//! +//! // Create the verification instruction +//! let instruction = offsets_to_falcon512_instruction(&[offsets]); +//! assert_eq!(instruction.data[0], 1); // num_signatures +//! ``` +//! +//! # Constants +//! +//! All size and header constants are provisional per draft FIPS 206 and may +//! change when the final standard is published: +//! +//! - [`PUBKEY_SIZE`]: 897 bytes (includes 1-byte header) +//! - [`PUBKEY_HEADER`][]: 0x09 (identifies Falcon-512 public keys) +//! - [`MIN_SIGNATURE_SIZE`]: 41 bytes minimum +//! - [`MAX_SIGNATURE_SIZE`]: 666 bytes maximum +//! - [`SIGNATURE_HEADER`][]: 0x39 (identifies Falcon-512 signatures) +//! +//! # Platform Support +//! +//! Cryptographic operations ([`SecretKey`], [`PublicKey::verify`]) are only +//! available on non-Solana targets (`cfg(not(target_os = "solana"))`). +//! Data types ([`PublicKey`], [`Signature`], [`Falcon512SignatureOffsets`]) +//! are available on all platforms. + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(not(feature = "std"))] +extern crate alloc; + +mod constants; +mod error; +mod instruction; +mod offsets; +mod public_key; +#[cfg(not(target_os = "solana"))] +mod secret_key; +mod signature; + +#[cfg(test)] +mod tests; + +#[cfg(not(target_os = "solana"))] +pub use secret_key::SecretKey; +pub use { + constants::{ + DATA_START, MAX_SIGNATURE_SIZE, MIN_SIGNATURE_SIZE, PUBKEY_HEADER, PUBKEY_SIZE, + SIGNATURE_HEADER, SIGNATURE_OFFSETS_SIZE, SIGNATURE_OFFSETS_START, + }, + error::FalconError, + instruction::{new_falcon512_instruction_with_signature, offsets_to_falcon512_instruction}, + offsets::Falcon512SignatureOffsets, + public_key::PublicKey, + signature::Signature, +}; diff --git a/falcon-signature/src/offsets.rs b/falcon-signature/src/offsets.rs new file mode 100644 index 000000000..148736ac9 --- /dev/null +++ b/falcon-signature/src/offsets.rs @@ -0,0 +1,50 @@ +use bytemuck_derive::{Pod, Zeroable}; + +/// Offsets for locating Falcon-512 signature data within instruction data. +/// +/// This structure follows the SIMD-0152 precompile pattern, allowing signatures, +/// public keys, and messages to be located either in the current instruction +/// or in other instructions within the same transaction. +/// +/// The instruction index fields use `u16::MAX` to indicate the current instruction. +/// Values less than `u16::MAX` reference prior instructions in the transaction. +/// +/// # Example +/// +/// ``` +/// use solana_falcon_signature::{Falcon512SignatureOffsets, SIGNATURE_OFFSETS_SIZE}; +/// +/// let offsets = Falcon512SignatureOffsets { +/// signature_offset: 18, +/// signature_length: 650, +/// signature_instruction_index: u16::MAX, // current instruction +/// public_key_offset: 668, +/// public_key_instruction_index: u16::MAX, +/// message_offset: 1565, +/// message_length: 100, +/// message_instruction_index: u16::MAX, +/// }; +/// +/// // Structure is exactly 16 bytes for bytemuck compatibility +/// assert_eq!(std::mem::size_of::(), SIGNATURE_OFFSETS_SIZE); +/// ``` +#[derive(Default, Debug, Copy, Clone, Zeroable, Pod, Eq, PartialEq)] +#[repr(C)] +pub struct Falcon512SignatureOffsets { + /// Offset to the signature within the instruction data. + pub signature_offset: u16, + /// Length of the signature in bytes. + pub signature_length: u16, + /// Instruction index containing the signature (u16::MAX = current instruction). + pub signature_instruction_index: u16, + /// Offset to the public key within the instruction data. + pub public_key_offset: u16, + /// Instruction index containing the public key (u16::MAX = current instruction). + pub public_key_instruction_index: u16, + /// Offset to the message within the instruction data. + pub message_offset: u16, + /// Length of the message in bytes. + pub message_length: u16, + /// Instruction index containing the message (u16::MAX = current instruction). + pub message_instruction_index: u16, +} diff --git a/falcon-signature/src/public_key.rs b/falcon-signature/src/public_key.rs new file mode 100644 index 000000000..4220fd88b --- /dev/null +++ b/falcon-signature/src/public_key.rs @@ -0,0 +1,83 @@ +use crate::{ + constants::{PUBKEY_HEADER, PUBKEY_SIZE}, + error::FalconError, + signature::Signature, +}; + +/// A Falcon-512 public key. +/// +/// The public key is 897 bytes, with the first byte being the header (0x09). +/// +/// # Example +/// +/// ``` +/// use solana_falcon_signature::{PublicKey, PUBKEY_SIZE, PUBKEY_HEADER, FalconError}; +/// +/// let mut bytes = [0u8; PUBKEY_SIZE]; +/// bytes[0] = PUBKEY_HEADER; +/// let pk = PublicKey::new(bytes).expect("valid public key"); +/// assert_eq!(pk.as_bytes()[0], PUBKEY_HEADER); +/// +/// let mut bad = bytes; +/// bad[0] = 0xFF; +/// assert!(matches!(PublicKey::new(bad), Err(FalconError::InvalidPublicKeyHeader(0xFF)))); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublicKey([u8; PUBKEY_SIZE]); + +impl PublicKey { + /// Creates a new public key from a fixed-size array. + /// + /// Returns an error if the header byte is invalid. + pub fn new(bytes: [u8; PUBKEY_SIZE]) -> Result { + if bytes[0] != PUBKEY_HEADER { + return Err(FalconError::InvalidPublicKeyHeader(bytes[0])); + } + Ok(Self(bytes)) + } + + /// Creates a public key from a slice. + /// + /// Returns an error if the size is incorrect or the header byte is invalid. + pub fn from_slice(bytes: &[u8]) -> Result { + if bytes.len() != PUBKEY_SIZE { + return Err(FalconError::InvalidPublicKeySize(bytes.len())); + } + if bytes[0] != PUBKEY_HEADER { + return Err(FalconError::InvalidPublicKeyHeader(bytes[0])); + } + let mut arr = [0u8; PUBKEY_SIZE]; + arr.copy_from_slice(bytes); + Ok(Self(arr)) + } + + /// Returns the public key as a byte slice. + pub fn as_bytes(&self) -> &[u8; PUBKEY_SIZE] { + &self.0 + } + + /// Verifies a signature over a message. + #[cfg(not(target_os = "solana"))] + pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), FalconError> { + use oqs::sig::{Algorithm, Sig}; + + let sig = Sig::new(Algorithm::Falcon512).map_err(|_| FalconError::VerificationFailed)?; + + let oqs_pk = sig + .public_key_from_bytes(&self.0) + .ok_or(FalconError::VerificationFailed)?; + + let oqs_sig = sig + .signature_from_bytes(signature.as_bytes()) + .ok_or(FalconError::VerificationFailed)?; + + sig.verify(message, oqs_sig, oqs_pk) + .map_err(|_| FalconError::VerificationFailed) + } +} + +impl AsRef<[u8]> for PublicKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/falcon-signature/src/secret_key.rs b/falcon-signature/src/secret_key.rs new file mode 100644 index 000000000..564c93948 --- /dev/null +++ b/falcon-signature/src/secret_key.rs @@ -0,0 +1,51 @@ +use crate::{error::FalconError, public_key::PublicKey, signature::Signature}; + +/// A Falcon-512 secret key. +/// +/// This type is only available on non-Solana targets as cryptographic +/// operations require the liboqs library. +pub struct SecretKey { + inner: oqs::sig::SecretKey, + public_key: PublicKey, +} + +impl SecretKey { + /// Generates a new random keypair. + pub fn generate() -> Result { + use oqs::sig::{Algorithm, Sig}; + + let sig = Sig::new(Algorithm::Falcon512) + .map_err(|_| FalconError::KeyGenerationFailed("failed to initialize Falcon512"))?; + + let (pk, sk) = sig + .keypair() + .map_err(|_| FalconError::KeyGenerationFailed("keypair generation failed"))?; + + let pk_bytes = pk.as_ref(); + let public_key = PublicKey::from_slice(pk_bytes)?; + + Ok(Self { + inner: sk, + public_key, + }) + } + + /// Returns the public key corresponding to this secret key. + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } + + /// Signs a message with this secret key. + pub fn sign(&self, message: &[u8]) -> Result { + use oqs::sig::{Algorithm, Sig}; + + let sig = Sig::new(Algorithm::Falcon512) + .map_err(|_| FalconError::SigningFailed("failed to initialize Falcon512"))?; + + let oqs_sig = sig + .sign(message, &self.inner) + .map_err(|_| FalconError::SigningFailed("signing operation failed"))?; + + Signature::new(oqs_sig.as_ref().to_vec()) + } +} diff --git a/falcon-signature/src/signature.rs b/falcon-signature/src/signature.rs new file mode 100644 index 000000000..301f0d6ed --- /dev/null +++ b/falcon-signature/src/signature.rs @@ -0,0 +1,85 @@ +use crate::{ + constants::{MAX_SIGNATURE_SIZE, MIN_SIGNATURE_SIZE, SIGNATURE_HEADER}, + error::FalconError, +}; +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; + +/// A Falcon-512 signature. +/// +/// Signatures are variable length (41-666 bytes), with the first byte being +/// the header (0x39). +/// +/// # Example +/// +/// ``` +/// use solana_falcon_signature::{ +/// Signature, MIN_SIGNATURE_SIZE, MAX_SIGNATURE_SIZE, SIGNATURE_HEADER, FalconError +/// }; +/// +/// // Create a signature with valid header and size +/// let bytes = vec![SIGNATURE_HEADER; 100]; +/// let sig = Signature::new(bytes).expect("valid signature"); +/// assert!(sig.len() >= MIN_SIGNATURE_SIZE && sig.len() <= MAX_SIGNATURE_SIZE); +/// +/// // Too small signature is rejected +/// let small = vec![SIGNATURE_HEADER; 10]; +/// assert!(matches!(Signature::new(small), Err(FalconError::InvalidSignatureSize(10)))); +/// +/// // Invalid header is rejected +/// let bad_header = vec![0xFF; 100]; +/// assert!(matches!(Signature::new(bad_header), Err(FalconError::InvalidSignatureHeader(0xFF)))); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signature(Vec); + +impl Signature { + /// Creates a new signature from raw bytes. + /// + /// Returns an error if the size is out of range or the header byte is invalid. + pub fn new(bytes: Vec) -> Result { + let len = bytes.len(); + if !(MIN_SIGNATURE_SIZE..=MAX_SIGNATURE_SIZE).contains(&len) { + return Err(FalconError::InvalidSignatureSize(len)); + } + if bytes[0] != SIGNATURE_HEADER { + return Err(FalconError::InvalidSignatureHeader(bytes[0])); + } + Ok(Self(bytes)) + } + + /// Creates a signature from a slice. + /// + /// Returns an error if the size is out of range or the header byte is invalid. + pub fn from_slice(bytes: &[u8]) -> Result { + let len = bytes.len(); + if !(MIN_SIGNATURE_SIZE..=MAX_SIGNATURE_SIZE).contains(&len) { + return Err(FalconError::InvalidSignatureSize(len)); + } + if bytes[0] != SIGNATURE_HEADER { + return Err(FalconError::InvalidSignatureHeader(bytes[0])); + } + Ok(Self(bytes.to_vec())) + } + + /// Returns the signature as a byte slice. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Returns the length of the signature in bytes. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns true if the signature is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/falcon-signature/src/tests.rs b/falcon-signature/src/tests.rs new file mode 100644 index 000000000..f591b4402 --- /dev/null +++ b/falcon-signature/src/tests.rs @@ -0,0 +1,312 @@ +use super::*; + +/// AC-1.0: Verify oqs output format matches FIPS 206 expectations. +/// +/// This executable test verifies that oqs returns public keys and signatures +/// with the expected header bytes and sizes. If these tests fail, the wrapper +/// implementation must adapt to add/strip headers. +#[test] +fn test_oqs_output_format() { + // Generate a keypair + let sk = SecretKey::generate().expect("key generation should succeed"); + let pk = sk.public_key(); + + // AC-1.0: oqs PublicKey::as_ref() returns exactly PUBKEY_SIZE bytes + let pk_bytes = pk.as_bytes(); + assert_eq!( + pk_bytes.len(), + PUBKEY_SIZE, + "oqs public key should be {PUBKEY_SIZE} bytes" + ); + + // AC-1.0: oqs PublicKey::as_ref()[0] equals PUBKEY_HEADER + assert_eq!( + pk_bytes[0], PUBKEY_HEADER, + "oqs public key first byte should be {PUBKEY_HEADER:#04x}, got {:#04x}", + pk_bytes[0] + ); + + // Sign a test message + let message = b"test message for format verification"; + let sig = sk.sign(message).expect("signing should succeed"); + let sig_bytes = sig.as_bytes(); + + // AC-1.0: oqs signature length is within MIN_SIGNATURE_SIZE..=MAX_SIGNATURE_SIZE + assert!( + sig_bytes.len() >= MIN_SIGNATURE_SIZE && sig_bytes.len() <= MAX_SIGNATURE_SIZE, + "oqs signature size {} should be in range {}..={}", + sig_bytes.len(), + MIN_SIGNATURE_SIZE, + MAX_SIGNATURE_SIZE + ); + + // AC-1.0: oqs Signature::as_ref()[0] equals SIGNATURE_HEADER + assert_eq!( + sig_bytes[0], SIGNATURE_HEADER, + "oqs signature first byte should be {SIGNATURE_HEADER:#04x}, got {:#04x}", + sig_bytes[0] + ); +} + +#[test] +fn test_keypair_generation() { + let sk = SecretKey::generate().expect("key generation should succeed"); + let pk = sk.public_key(); + + assert_eq!(pk.as_bytes().len(), PUBKEY_SIZE); + assert_eq!(pk.as_bytes()[0], PUBKEY_HEADER); +} + +/// Self-signed verification: sign with key A, verify with key A succeeds. +#[test] +fn test_self_signed_verification() { + let sk = SecretKey::generate().expect("key generation should succeed"); + let pk = sk.public_key(); + + let message = b"Hello, Falcon-512!"; + let signature = sk.sign(message).expect("signing should succeed"); + + assert!(signature.len() >= MIN_SIGNATURE_SIZE); + assert!(signature.len() <= MAX_SIGNATURE_SIZE); + assert_eq!(signature.as_bytes()[0], SIGNATURE_HEADER); + + pk.verify(message, &signature) + .expect("self-signed verification should succeed"); +} + +/// Cross-key verification: sign with key A, verify with key B fails (proves keys are distinct). +#[test] +fn test_cross_key_verification_fails() { + let sk_a = SecretKey::generate().expect("key generation should succeed"); + let sk_b = SecretKey::generate().expect("key generation should succeed"); + + // Ensure keys are distinct + assert_ne!( + sk_a.public_key().as_bytes(), + sk_b.public_key().as_bytes(), + "generated keys should be distinct" + ); + + let message = b"Test message for cross-key verification"; + let signature = sk_a.sign(message).expect("signing should succeed"); + + // Verification with different key should fail + let result = sk_b.public_key().verify(message, &signature); + assert!( + result.is_err(), + "cross-key verification should fail (signature from key A, verified with key B)" + ); +} + +#[test] +fn test_verify_wrong_message() { + let sk = SecretKey::generate().expect("key generation should succeed"); + let pk = sk.public_key(); + + let message = b"Original message"; + let signature = sk.sign(message).expect("signing should succeed"); + + let wrong_message = b"Wrong message"; + let result = pk.verify(wrong_message, &signature); + assert!( + result.is_err(), + "verification with wrong message should fail" + ); +} + +#[test] +fn test_invalid_public_key_size() { + let bytes = vec![0u8; 100]; // Wrong size + let result = PublicKey::from_slice(&bytes); + assert!(matches!( + result, + Err(FalconError::InvalidPublicKeySize(100)) + )); +} + +#[test] +fn test_invalid_public_key_header() { + let mut bytes = [0u8; PUBKEY_SIZE]; + bytes[0] = 0xFF; // Wrong header + let result = PublicKey::new(bytes); + assert!(matches!( + result, + Err(FalconError::InvalidPublicKeyHeader(0xFF)) + )); +} + +#[test] +fn test_invalid_signature_size_too_small() { + let bytes = vec![SIGNATURE_HEADER; 10]; // Too small + let result = Signature::new(bytes); + assert!(matches!(result, Err(FalconError::InvalidSignatureSize(10)))); +} + +#[test] +fn test_invalid_signature_size_too_large() { + let bytes = vec![SIGNATURE_HEADER; 1000]; // Too large + let result = Signature::new(bytes); + assert!(matches!( + result, + Err(FalconError::InvalidSignatureSize(1000)) + )); +} + +#[test] +fn test_invalid_signature_header() { + let mut bytes = vec![0u8; 100]; + bytes[0] = 0xFF; // Wrong header + let result = Signature::new(bytes); + assert!(matches!( + result, + Err(FalconError::InvalidSignatureHeader(0xFF)) + )); +} + +#[test] +fn test_signature_offsets_size() { + assert_eq!( + core::mem::size_of::(), + SIGNATURE_OFFSETS_SIZE, + "Falcon512SignatureOffsets should be exactly {SIGNATURE_OFFSETS_SIZE} bytes" + ); +} + +#[test] +fn test_offsets_bytemuck_serialization() { + let offsets = Falcon512SignatureOffsets { + signature_offset: 100, + signature_length: 650, + signature_instruction_index: u16::MAX, + public_key_offset: 200, + public_key_instruction_index: u16::MAX, + message_offset: 300, + message_length: 32, + message_instruction_index: u16::MAX, + }; + + let bytes = bytemuck::bytes_of(&offsets); + assert_eq!(bytes.len(), SIGNATURE_OFFSETS_SIZE); + + // Verify little-endian encoding + assert_eq!(u16::from_le_bytes([bytes[0], bytes[1]]), 100); // signature_offset + assert_eq!(u16::from_le_bytes([bytes[2], bytes[3]]), 650); // signature_length +} + +#[test] +fn test_offsets_to_falcon512_instruction() { + let offsets = vec![Falcon512SignatureOffsets { + signature_offset: 18, + signature_length: 650, + signature_instruction_index: u16::MAX, + public_key_offset: 668, + public_key_instruction_index: u16::MAX, + message_offset: 1565, + message_length: 100, + message_instruction_index: u16::MAX, + }]; + + let instruction = offsets_to_falcon512_instruction(&offsets); + + // Verify program ID + assert_eq!( + instruction.program_id, + solana_sdk_ids::falcon512_program::id() + ); + + // Verify no accounts required + assert!(instruction.accounts.is_empty()); + + // Verify instruction data layout + assert_eq!(instruction.data[0], 1); // num_signatures + assert_eq!(instruction.data[1], 0); // padding + assert_eq!( + instruction.data.len(), + SIGNATURE_OFFSETS_START + SIGNATURE_OFFSETS_SIZE + ); +} + +#[test] +fn test_new_falcon512_instruction_with_signature() { + let sk = SecretKey::generate().expect("key generation should succeed"); + let pk = sk.public_key(); + let message = b"Test message for instruction"; + let signature = sk.sign(message).expect("signing should succeed"); + + let instruction = new_falcon512_instruction_with_signature(message, &signature, pk); + + // Verify program ID + assert_eq!( + instruction.program_id, + solana_sdk_ids::falcon512_program::id() + ); + + // Verify no accounts required + assert!(instruction.accounts.is_empty()); + + // Verify instruction data layout + assert_eq!(instruction.data[0], 1); // num_signatures + assert_eq!(instruction.data[1], 0); // padding + + // Verify expected data length + let expected_len = DATA_START + PUBKEY_SIZE + signature.len() + message.len(); + assert_eq!(instruction.data.len(), expected_len); + + // Verify public key is at expected offset + let pk_start = DATA_START; + let pk_end = pk_start + PUBKEY_SIZE; + assert_eq!(&instruction.data[pk_start..pk_end], pk.as_bytes()); + + // Verify signature is at expected offset + let sig_start = pk_end; + let sig_end = sig_start + signature.len(); + assert_eq!(&instruction.data[sig_start..sig_end], signature.as_bytes()); + + // Verify message is at expected offset + let msg_start = sig_end; + assert_eq!(&instruction.data[msg_start..], message); +} + +#[test] +fn test_instruction_offsets_match_data_layout() { + let sk = SecretKey::generate().expect("key generation should succeed"); + let pk = sk.public_key(); + let message = b"Verify offsets match actual data positions"; + let signature = sk.sign(message).expect("signing should succeed"); + + let instruction = new_falcon512_instruction_with_signature(message, &signature, pk); + + // Parse offsets from instruction data + let offsets_bytes = &instruction.data[SIGNATURE_OFFSETS_START..DATA_START]; + let offsets: &Falcon512SignatureOffsets = bytemuck::from_bytes(offsets_bytes); + + // Verify offsets point to correct data + let sig_start = offsets.signature_offset as usize; + let sig_end = sig_start + offsets.signature_length as usize; + assert_eq!( + &instruction.data[sig_start..sig_end], + signature.as_bytes(), + "signature offset should point to signature data" + ); + + let pk_start = offsets.public_key_offset as usize; + let pk_end = pk_start + PUBKEY_SIZE; + assert_eq!( + &instruction.data[pk_start..pk_end], + pk.as_bytes(), + "public key offset should point to public key data" + ); + + let msg_start = offsets.message_offset as usize; + let msg_end = msg_start + offsets.message_length as usize; + assert_eq!( + &instruction.data[msg_start..msg_end], + message, + "message offset should point to message data" + ); + + // Verify instruction indices are set to current instruction + assert_eq!(offsets.signature_instruction_index, u16::MAX); + assert_eq!(offsets.public_key_instruction_index, u16::MAX); + assert_eq!(offsets.message_instruction_index, u16::MAX); +} diff --git a/message/src/sanitized.rs b/message/src/sanitized.rs index 279d5e057..fc68f2850 100644 --- a/message/src/sanitized.rs +++ b/message/src/sanitized.rs @@ -9,7 +9,7 @@ use { solana_hash::Hash, solana_instruction::{BorrowedAccountMeta, BorrowedInstruction}, solana_sanitize::Sanitize, - solana_sdk_ids::{ed25519_program, secp256k1_program, secp256r1_program}, + solana_sdk_ids::{ed25519_program, falcon512_program, secp256k1_program, secp256r1_program}, solana_transaction_error::SanitizeMessageError, std::{borrow::Cow, collections::HashSet, convert::TryFrom}, }; @@ -380,6 +380,13 @@ impl SanitizedMessage { .num_secp256r1_instruction_signatures .saturating_add(u64::from(*num_verifies)); } + } else if falcon512_program::check_id(program_id) { + if let Some(num_verifies) = instruction.data.first() { + transaction_signature_details.num_falcon512_instruction_signatures = + transaction_signature_details + .num_falcon512_instruction_signatures + .saturating_add(u64::from(*num_verifies)); + } } } @@ -395,6 +402,7 @@ pub struct TransactionSignatureDetails { num_secp256k1_instruction_signatures: u64, num_ed25519_instruction_signatures: u64, num_secp256r1_instruction_signatures: u64, + num_falcon512_instruction_signatures: u64, } impl TransactionSignatureDetails { @@ -403,12 +411,14 @@ impl TransactionSignatureDetails { num_secp256k1_instruction_signatures: u64, num_ed25519_instruction_signatures: u64, num_secp256r1_instruction_signatures: u64, + num_falcon512_instruction_signatures: u64, ) -> Self { Self { num_transaction_signatures, num_secp256k1_instruction_signatures, num_ed25519_instruction_signatures, num_secp256r1_instruction_signatures, + num_falcon512_instruction_signatures, } } @@ -418,6 +428,7 @@ impl TransactionSignatureDetails { .saturating_add(self.num_secp256k1_instruction_signatures) .saturating_add(self.num_ed25519_instruction_signatures) .saturating_add(self.num_secp256r1_instruction_signatures) + .saturating_add(self.num_falcon512_instruction_signatures) } /// return the number of transaction signatures @@ -439,6 +450,11 @@ impl TransactionSignatureDetails { pub fn num_secp256r1_instruction_signatures(&self) -> u64 { self.num_secp256r1_instruction_signatures } + + /// return the number of falcon512 instruction signatures + pub fn num_falcon512_instruction_signatures(&self) -> u64 { + self.num_falcon512_instruction_signatures + } } #[cfg(test)] @@ -667,6 +683,56 @@ mod tests { assert_eq!(5, signature_details.num_ed25519_instruction_signatures); } + #[test] + fn test_get_signature_details_falcon512() { + let key0 = Address::new_unique(); + let key1 = Address::new_unique(); + let loader_key = Address::new_unique(); + + let loader_instr = CompiledInstruction::new(2, &(), vec![0, 1]); + // Mock falcon512 instruction with 3 signatures (first byte is num_signatures) + let mock_falcon512_instr = CompiledInstruction::new(3, &[3u8; 10], vec![]); + + let message = SanitizedMessage::try_from_legacy_message( + legacy::Message::new_with_compiled_instructions( + 2, + 1, + 2, + vec![key0, key1, loader_key, falcon512_program::id()], + Hash::default(), + vec![ + loader_instr, + mock_falcon512_instr.clone(), + mock_falcon512_instr, + ], + ), + &HashSet::new(), + ) + .unwrap(); + + let signature_details = message.get_signature_details(); + // expect 2 required transaction signatures + assert_eq!(2, signature_details.num_transaction_signatures()); + // expect 6 falcon512 instruction signatures (3 from each mock_falcon512_instr) + assert_eq!(6, signature_details.num_falcon512_instruction_signatures()); + // expect 0 for other precompile signatures + assert_eq!(0, signature_details.num_ed25519_instruction_signatures()); + assert_eq!(0, signature_details.num_secp256k1_instruction_signatures()); + assert_eq!(0, signature_details.num_secp256r1_instruction_signatures()); + // total should include all signatures + assert_eq!(8, signature_details.total_signatures()); + } + + #[test] + fn test_falcon512_check_id() { + // check_id returns true for falcon512 program ID + assert!(falcon512_program::check_id(&falcon512_program::id())); + // check_id returns false for other program IDs + assert!(!falcon512_program::check_id(&ed25519_program::id())); + assert!(!falcon512_program::check_id(&secp256k1_program::id())); + assert!(!falcon512_program::check_id(&Address::new_unique())); + } + #[test] fn test_static_account_keys() { let keys = vec![ diff --git a/program/src/falcon512_program.rs b/program/src/falcon512_program.rs new file mode 100644 index 000000000..9eac90d0b --- /dev/null +++ b/program/src/falcon512_program.rs @@ -0,0 +1,4 @@ +//! The [falcon512 native program][np]. +//! +//! [np]: https://docs.solanalabs.com/runtime/programs#falcon512-program +pub use solana_sdk_ids::falcon512_program::{check_id, id, ID}; diff --git a/program/src/lib.rs b/program/src/lib.rs index 70844d2f2..a5e7f911b 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -476,6 +476,7 @@ pub mod compute_units; pub mod ed25519_program; pub mod entrypoint_deprecated; pub mod epoch_schedule; +pub mod falcon512_program; pub use solana_epoch_stake as epoch_stake; pub mod hash; pub mod incinerator; diff --git a/sdk-ids/src/lib.rs b/sdk-ids/src/lib.rs index d161bb2d4..0e46050d4 100644 --- a/sdk-ids/src/lib.rs +++ b/sdk-ids/src/lib.rs @@ -29,6 +29,13 @@ pub mod ed25519_program { solana_address::declare_id!("Ed25519SigVerify111111111111111111111111111"); } +/// Falcon-512 signature verification program (post-quantum). +/// +/// Program ID is provisional and may change when SIMD proposal is finalized. +pub mod falcon512_program { + solana_address::declare_id!("Fa1con512SigVerify11111111111111111111111111"); +} + pub mod feature { solana_address::declare_id!("Feature111111111111111111111111111111111111"); } From 4148ee51c058567e8883efb54f1add08d160c59e Mon Sep 17 00:00:00 2001 From: zz-sol Date: Mon, 26 Jan 2026 14:47:43 -0500 Subject: [PATCH 2/8] add more tests --- Cargo.lock | 1 + falcon-signature/Cargo.toml | 1 + falcon-signature/src/tests.rs | 242 +++++++++++++++++++++++++++++++++- 3 files changed, 239 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1cc653945..39c625bcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3429,6 +3429,7 @@ dependencies = [ name = "solana-falcon-signature" version = "0.1.0" dependencies = [ + "base64 0.22.1", "bytemuck", "bytemuck_derive", "oqs", diff --git a/falcon-signature/Cargo.toml b/falcon-signature/Cargo.toml index 74870bb75..5c3bd1f50 100644 --- a/falcon-signature/Cargo.toml +++ b/falcon-signature/Cargo.toml @@ -23,6 +23,7 @@ thiserror = { workspace = true } oqs = { workspace = true } [dev-dependencies] +base64 = { workspace = true } rand = { workspace = true } [lints] diff --git a/falcon-signature/src/tests.rs b/falcon-signature/src/tests.rs index f591b4402..9f1acf382 100644 --- a/falcon-signature/src/tests.rs +++ b/falcon-signature/src/tests.rs @@ -1,6 +1,238 @@ use super::*; +use base64::Engine; + +// ============================================================================ +// Pre-computed test vectors (generated once, used for testing without sign()) +// ============================================================================ +// These test vectors allow verification testing without requiring the sign() +// function to be implemented or available. The vectors were generated using +// the `generate_test_vectors_for_hardcoding` test below. +// +// Note: Known Answer Tests (KAT) for Falcon-512 are performed in liboqs itself, +// so we skip them in this repository. Our tests focus on the SDK wrapper layer: +// serialization, deserialization, instruction building, and integration with +// the Solana runtime. + +const TEST_MESSAGE: &[u8] = b"test message for pre-computed verification"; + +// Public key (897 bytes) +const TEST_PUBKEY_BASE64: &str = "CV6ZIYJ17ttDNY6BY2L8laqhQqEQT0hhvZsBqe1NNeUj+c7ULeEiVl3FZwC0xqYFDh9uAJxQyM1eGQ3MeH9TzRTuCQiF0W21vlwR0R/Rt3ClEYJaopd4q5fnosNXxfOmS4pgBxLB2wgT7KLct2MOp+s1RClmOynIr54jA5YIrE0w/juE1Xmp3Yc3igsaJumDqBCUthmKQLaVa6+wKNNnAFtaNbHbuGhIQaC9edrcknM4dBlJzWp8YDIqxOHs3aou3+ljgHZjmGW3VYcgcf2TK8SRtVmQVHiCkQa2woZ204kRkXUIOuMRUmb8B+K53kALVjDJ2ZvTuKIukFqO0idUMmFzyO+K0vDBlMysF9yDGZ/Um1US+Hk6hyqpyFyz4J+hYhaD0vtK7Qf0UGsVU3GEcOLB9MWUCuxEkmQttxU4SbEFVtWy1M/BvU2AoPl0N8KlQbm5+IvrEA86UFfHHykhGBUYIpPdxiOmlvM5hlce6Q3UgB+dCuy61uUYD3yI3+Z/RSEaBWBQ92MCt9ImtyCVjiRNWaM53hlTSF+7t0AlVay5G/Lf0UOyEScNSaVe4GJTv9vxnxyaFL9c+4k03abAVFC9prS5lCDLGpnUqvGQGUemP9hG7eY3PWOqgB+DEHhVmkIU4ybJAHbPFL0BxubCO2MxW+mf+pyi2CQK7ypAH30ZtpQ3/Z2La45d6voM2n9s9hddXm1wjXnh5EXWv7mCKoXK2IExRbshnnAp+pJ5jEVYsFKB6IqRo4i+YRKuyU4EEXDVZSB9URRpkm6pMTlULvPmYMojEMAkVRt4Y7ZoyQNQIT5gs2hGUZ2+8jwT2KW7t7k1MhjeWC5uRmdrgyYGNfN609MlkQi7IIDgrwpF+8WrzemR5vcmySSVo3Hk9WjjrrnI3UjvLKSylRrJdgG9WMD3NE8RCjGRwWQoXzK4kPuBs1NnvX2ywDxhhYFwpKOQ338UcTg4lhhNpzzGmOGP3LXuxrCK5AGf+erwsJETJhWu5TcihpzmY/bgw76Vei9S7WVWqx0opviNhQbmzboQwQRxBX2Wewgmvz2I86pKfXgyJfQ7gOae0MrrGmDnkQ3W9ENhwrJ1G7fmofMrnnzFOVqbZJq9Wz2fnYmSY2c0URS9FmxUC5fGI5Kx01cZnyh6AxSGBnxATi/FR1lhKvx+NdsCM2pEIHC5ypFIiazAqIEC"; + +// Signature (655 bytes) +const TEST_SIGNATURE_BASE64: &str = "OSCJBLuOJh5ASZPOMj7hL6Wvqs4nmwXV+xEGkRUkLiXgHaXqBjaayVwEYSQ6SDYKA5oi1aWzmx5uULs0Xw/l7+VsG58jhGL5NfkO0PPK9dH/R6HBhGamGPOqTW9Ye/0PeWUydbcRQ8XKWEV7IazO0+pmahhul7gSWcvZnJVbVf14e/UITCvmsXWnr1wQk0tMscaDPGwLvsr1MevbAzxUZlZ3SZ80D9YEyI1dQ5O6NrnpZlifJfgszEHXuRjtV6iCmNLBojckK0GCe/55ddEHKz/uLREPyXav5qM5jczwdjMq43qp50UshSHJzLtCY9LWfNpvp882VkCrXLEC/oQ0elIO2bXbhSYX+sev1t0qS0pNMtkmCFi/sHpNIUqr8Cazw7+MiZzUsTxHTRRjIKjhsdrEh6Oe0vi96h2d86iyGxo5TGZtNYg729HQnPRCFCEy1OHTPl6NL3s8VbyX7Ak/hqg2SDzy7pDofHSz9YNd4ftr5x9Cia+K3rVJZxm3M16c2NvNusblH5xpvGHUVKnJUZNlArhU3r3kW2eQZzqOoj5jOFC65kNxJm2yZUoA05Fq//oCT+sw1TDLrtJjKupDIIbhs4o1iIQCDMKVmFs1OiG5g/Kf5zMQE4f4huuiSn4dTsk9PArlGpvpnOLRiY+DUSRgHeKB7twZORI+qBoI2SnTwPhT+DE6WyM3VC13+i3VBVbNv9HNe3/rnBEa6ZtzZMtPnmQs2eZ26L83mcDONoxwvm/rsfKxNEoO7ivdBG3l52URskV5lbgbLNnfldnfn7eFkiCHikUl1m3INgiTiB83KnRauvYW4FWaDhs2xvX8qL7upp3mFnuH6cnGenXVFjFVcG5t0vm6YW/ObDyIkA=="; + +/// Helper function to decode base64 test vectors +fn decode_test_pubkey() -> PublicKey { + let b64 = base64::engine::general_purpose::STANDARD; + let bytes = b64.decode(TEST_PUBKEY_BASE64).expect("valid base64"); + PublicKey::from_slice(&bytes).expect("valid public key") +} + +/// Helper function to decode base64 test vectors +fn decode_test_signature() -> Signature { + let b64 = base64::engine::general_purpose::STANDARD; + let bytes = b64.decode(TEST_SIGNATURE_BASE64).expect("valid base64"); + Signature::from_slice(&bytes).expect("valid signature") +} + +// ============================================================================ +// Tests using pre-computed test vectors (no sign() required) +// ============================================================================ + +/// Test that pre-computed public key bytes can be deserialized correctly. +#[test] +fn test_precomputed_pubkey_deserialization() { + let pk = decode_test_pubkey(); + + assert_eq!(pk.as_bytes().len(), PUBKEY_SIZE); + assert_eq!(pk.as_bytes()[0], PUBKEY_HEADER); +} + +/// Test that pre-computed signature bytes can be deserialized correctly. +#[test] +fn test_precomputed_signature_deserialization() { + let sig = decode_test_signature(); + + assert!(sig.len() >= MIN_SIGNATURE_SIZE); + assert!(sig.len() <= MAX_SIGNATURE_SIZE); + assert_eq!(sig.as_bytes()[0], SIGNATURE_HEADER); +} + +/// Test verification with pre-computed test vectors. +/// This test validates the verify() function without requiring sign(). +#[test] +fn test_precomputed_verification_succeeds() { + let pk = decode_test_pubkey(); + let sig = decode_test_signature(); + + pk.verify(TEST_MESSAGE, &sig) + .expect("verification with pre-computed test vectors should succeed"); +} + +/// Test that verification fails with wrong message using pre-computed vectors. +#[test] +fn test_precomputed_verification_wrong_message_fails() { + let pk = decode_test_pubkey(); + let sig = decode_test_signature(); + + let wrong_message = b"wrong message"; + let result = pk.verify(wrong_message, &sig); + assert!( + result.is_err(), + "verification should fail with wrong message" + ); +} + +/// Test that verification fails with corrupted signature. +#[test] +fn test_precomputed_verification_corrupted_signature_fails() { + let pk = decode_test_pubkey(); + let b64 = base64::engine::general_purpose::STANDARD; + let mut sig_bytes = b64.decode(TEST_SIGNATURE_BASE64).expect("valid base64"); + + // Corrupt a byte in the middle of the signature (not the header) + sig_bytes[100] ^= 0xFF; + + let corrupted_sig = Signature::from_slice(&sig_bytes).expect("still valid format"); + let result = pk.verify(TEST_MESSAGE, &corrupted_sig); + assert!( + result.is_err(), + "verification should fail with corrupted signature" + ); +} + +/// Test that verification fails with corrupted public key. +#[test] +fn test_precomputed_verification_corrupted_pubkey_fails() { + let b64 = base64::engine::general_purpose::STANDARD; + let mut pk_bytes: [u8; PUBKEY_SIZE] = b64 + .decode(TEST_PUBKEY_BASE64) + .expect("valid base64") + .try_into() + .expect("correct size"); + + // Corrupt a byte in the middle of the public key (not the header) + pk_bytes[100] ^= 0xFF; + + let corrupted_pk = PublicKey::new(pk_bytes).expect("still valid format"); + let sig = decode_test_signature(); + + let result = corrupted_pk.verify(TEST_MESSAGE, &sig); + assert!( + result.is_err(), + "verification should fail with corrupted public key" + ); +} + +/// Test instruction creation with pre-computed test vectors. +#[test] +fn test_precomputed_instruction_creation() { + let pk = decode_test_pubkey(); + let sig = decode_test_signature(); + + let instruction = new_falcon512_instruction_with_signature(TEST_MESSAGE, &sig, &pk); + + // Verify program ID + assert_eq!( + instruction.program_id, + solana_sdk_ids::falcon512_program::id() + ); + + // Verify expected data length + let expected_len = DATA_START + PUBKEY_SIZE + sig.len() + TEST_MESSAGE.len(); + assert_eq!(instruction.data.len(), expected_len); + + // Verify public key is at expected offset + let pk_start = DATA_START; + let pk_end = pk_start + PUBKEY_SIZE; + assert_eq!(&instruction.data[pk_start..pk_end], pk.as_bytes()); + + // Verify signature is at expected offset + let sig_start = pk_end; + let sig_end = sig_start + sig.len(); + assert_eq!(&instruction.data[sig_start..sig_end], sig.as_bytes()); + + // Verify message is at expected offset + let msg_start = sig_end; + assert_eq!(&instruction.data[msg_start..], TEST_MESSAGE); +} + +/// Test round-trip: verify instruction offsets point to correct data with pre-computed vectors. +#[test] +fn test_precomputed_instruction_offsets_roundtrip() { + let pk = decode_test_pubkey(); + let sig = decode_test_signature(); + + let instruction = new_falcon512_instruction_with_signature(TEST_MESSAGE, &sig, &pk); + + // Parse offsets from instruction data + let offsets_bytes = &instruction.data[SIGNATURE_OFFSETS_START..DATA_START]; + let offsets: &Falcon512SignatureOffsets = bytemuck::from_bytes(offsets_bytes); + + // Extract data using offsets + let extracted_sig_start = offsets.signature_offset as usize; + let extracted_sig_end = extracted_sig_start + offsets.signature_length as usize; + let extracted_sig_bytes = &instruction.data[extracted_sig_start..extracted_sig_end]; + + let extracted_pk_start = offsets.public_key_offset as usize; + let extracted_pk_end = extracted_pk_start + PUBKEY_SIZE; + let extracted_pk_bytes = &instruction.data[extracted_pk_start..extracted_pk_end]; + + let extracted_msg_start = offsets.message_offset as usize; + let extracted_msg_end = extracted_msg_start + offsets.message_length as usize; + let extracted_msg_bytes = &instruction.data[extracted_msg_start..extracted_msg_end]; + + // Reconstruct types from extracted bytes + let extracted_pk = PublicKey::from_slice(extracted_pk_bytes).expect("valid public key"); + let extracted_sig = Signature::from_slice(extracted_sig_bytes).expect("valid signature"); + + // Verify the extracted data matches originals + assert_eq!(extracted_pk.as_bytes(), pk.as_bytes()); + assert_eq!(extracted_sig.as_bytes(), sig.as_bytes()); + assert_eq!(extracted_msg_bytes, TEST_MESSAGE); + + // Verify the extracted components can be used for verification + extracted_pk + .verify(extracted_msg_bytes, &extracted_sig) + .expect("verification with extracted components should succeed"); +} + +#[test] +#[ignore] // Only run manually to generate new test vectors +fn generate_test_vectors_for_hardcoding() { + let b64 = base64::engine::general_purpose::STANDARD; + + let sk = SecretKey::generate().expect("key generation should succeed"); + let pk = sk.public_key(); + + let message = b"test message for pre-computed verification"; + let signature = sk.sign(message).expect("signing should succeed"); + + println!("\n=== TEST VECTORS ==="); + println!( + "const TEST_MESSAGE: &[u8] = b\"{}\";", + std::str::from_utf8(message).unwrap() + ); + println!(); + println!("// Public key ({} bytes)", pk.as_bytes().len()); + println!( + "const TEST_PUBKEY_BASE64: &str = \"{}\";", + b64.encode(pk.as_bytes()) + ); + println!(); + println!("// Signature ({} bytes)", signature.len()); + println!( + "const TEST_SIGNATURE_BASE64: &str = \"{}\";", + b64.encode(signature.as_bytes()) + ); + println!("=== END TEST VECTORS ===\n"); +} + +// ============================================================================ +// Normal tests +// ============================================================================ -/// AC-1.0: Verify oqs output format matches FIPS 206 expectations. +/// Verify oqs output format matches FIPS 206 expectations. /// /// This executable test verifies that oqs returns public keys and signatures /// with the expected header bytes and sizes. If these tests fail, the wrapper @@ -11,7 +243,7 @@ fn test_oqs_output_format() { let sk = SecretKey::generate().expect("key generation should succeed"); let pk = sk.public_key(); - // AC-1.0: oqs PublicKey::as_ref() returns exactly PUBKEY_SIZE bytes + // oqs PublicKey::as_ref() returns exactly PUBKEY_SIZE bytes let pk_bytes = pk.as_bytes(); assert_eq!( pk_bytes.len(), @@ -19,7 +251,7 @@ fn test_oqs_output_format() { "oqs public key should be {PUBKEY_SIZE} bytes" ); - // AC-1.0: oqs PublicKey::as_ref()[0] equals PUBKEY_HEADER + // oqs PublicKey::as_ref()[0] equals PUBKEY_HEADER assert_eq!( pk_bytes[0], PUBKEY_HEADER, "oqs public key first byte should be {PUBKEY_HEADER:#04x}, got {:#04x}", @@ -31,7 +263,7 @@ fn test_oqs_output_format() { let sig = sk.sign(message).expect("signing should succeed"); let sig_bytes = sig.as_bytes(); - // AC-1.0: oqs signature length is within MIN_SIGNATURE_SIZE..=MAX_SIGNATURE_SIZE + // oqs signature length is within MIN_SIGNATURE_SIZE..=MAX_SIGNATURE_SIZE assert!( sig_bytes.len() >= MIN_SIGNATURE_SIZE && sig_bytes.len() <= MAX_SIGNATURE_SIZE, "oqs signature size {} should be in range {}..={}", @@ -40,7 +272,7 @@ fn test_oqs_output_format() { MAX_SIGNATURE_SIZE ); - // AC-1.0: oqs Signature::as_ref()[0] equals SIGNATURE_HEADER + // oqs Signature::as_ref()[0] equals SIGNATURE_HEADER assert_eq!( sig_bytes[0], SIGNATURE_HEADER, "oqs signature first byte should be {SIGNATURE_HEADER:#04x}, got {:#04x}", From 4a53f6aff43f0b9ee4bca148a50be6a7ba9a6a9a Mon Sep 17 00:00:00 2001 From: zz-sol Date: Wed, 28 Jan 2026 10:12:37 -0500 Subject: [PATCH 3/8] clean up white space --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6db778204..6a4dc6f25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ members = [ "epoch-rewards-hasher", "epoch-schedule", "epoch-stake", - "example-mocks", + "example-mocks", "falcon-signature", "feature-gate-interface", "fee-calculator", From b12470356eae3e6b360abca28d1968f6142ef7bc Mon Sep 17 00:00:00 2001 From: zz-sol Date: Wed, 28 Jan 2026 10:15:11 -0500 Subject: [PATCH 4/8] fix CI --- falcon-signature/Cargo.toml | 5 +++++ falcon-signature/src/lib.rs | 1 + 2 files changed, 6 insertions(+) diff --git a/falcon-signature/Cargo.toml b/falcon-signature/Cargo.toml index 5c3bd1f50..8102301fe 100644 --- a/falcon-signature/Cargo.toml +++ b/falcon-signature/Cargo.toml @@ -8,6 +8,11 @@ license.workspace = true edition.workspace = true description = "Falcon-512 post-quantum signature support for Solana SDK" +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + [features] default = ["std"] std = [] diff --git a/falcon-signature/src/lib.rs b/falcon-signature/src/lib.rs index ba229d670..c55798caf 100644 --- a/falcon-signature/src/lib.rs +++ b/falcon-signature/src/lib.rs @@ -1,3 +1,4 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] //! Falcon-512 post-quantum signature support for Solana SDK. //! //! This crate provides Falcon-512 signature operations using the `oqs` crate From 2d8860ac08381fb2039706244cafe8b8b18e5df3 Mon Sep 17 00:00:00 2001 From: zz-sol Date: Wed, 28 Jan 2026 10:23:48 -0500 Subject: [PATCH 5/8] downgrade liboqs to be consistent with MSRV --- Cargo.lock | 86 ++++++++++++++++++++++++++++------- Cargo.toml | 2 +- falcon-signature/src/tests.rs | 3 +- 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39c625bcc..d1866b5b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -421,22 +421,25 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.71.1" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools 0.12.1", + "lazy_static", + "lazycell", "log", "prettyplease", "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn 2.0.111", + "which", ] [[package]] @@ -1611,6 +1614,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "0.2.12" @@ -1956,6 +1968,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.170" @@ -1996,6 +2014,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -2237,9 +2261,9 @@ dependencies = [ [[package]] name = "oqs" -version = "0.11.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48caac02cf42ba00b865a747e332828a75341d97ae35ad1ae9785e56de212e78" +checksum = "828f06d734c93f9ba75f0ae8075a7eb25d71c59705f2a7bf426ed997fe62beb5" dependencies = [ "cstr_core", "libc", @@ -2248,9 +2272,9 @@ dependencies = [ [[package]] name = "oqs-sys" -version = "0.11.0+liboqs-0.13.0" +version = "0.10.1+liboqs-0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6d66ee528a895ce5cc08851698d109c5d7ee5d7a0b3b40d61550eda91e414f" +checksum = "b4084714b15c8545e7dd2e9d1e4a5482547ece7476d07f4b5e96b5babb78c4dd" dependencies = [ "bindgen", "build-deps", @@ -2710,12 +2734,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - [[package]] name = "rustc_version" version = "0.4.1" @@ -2725,6 +2743,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.0.7" @@ -2734,7 +2765,7 @@ dependencies = [ "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] @@ -4668,7 +4699,7 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -4763,7 +4794,7 @@ dependencies = [ "once_cell", "pbkdf2 0.12.2", "rand 0.8.5", - "rustc-hash 1.1.0", + "rustc-hash", "sha2", "thiserror 1.0.69", "unicode-normalization", @@ -5140,6 +5171,18 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi-util" version = "0.1.9" @@ -5207,6 +5250,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 6a4dc6f25..1e028f3f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -196,7 +196,7 @@ num-derive = "0.4" num-traits = { version = "0.2.18", default-features = false } num_enum = "0.7.3" openssl = "0.10.72" -oqs = { version = "0.11", default-features = false, features = ["std", "falcon", "vendored"] } +oqs = { version = "0.10", default-features = false, features = ["std", "falcon", "vendored"] } pairing = "0.23.0" parking_lot = "0.12" pbkdf2 = { version = "0.11.0", default-features = false } diff --git a/falcon-signature/src/tests.rs b/falcon-signature/src/tests.rs index 9f1acf382..89185f5af 100644 --- a/falcon-signature/src/tests.rs +++ b/falcon-signature/src/tests.rs @@ -1,5 +1,4 @@ -use super::*; -use base64::Engine; +use {super::*, base64::Engine}; // ============================================================================ // Pre-computed test vectors (generated once, used for testing without sign()) From 6fda60e85690c379bbfdf038aab606effeec1d5a Mon Sep 17 00:00:00 2001 From: zz-sol Date: Wed, 28 Jan 2026 10:30:22 -0500 Subject: [PATCH 6/8] downgrade liboqs again to support rust 2021 edition --- Cargo.lock | 20 +++++++++++++------- Cargo.toml | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1866b5b5..565eb93fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -421,17 +421,17 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.5" +version = "0.68.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" dependencies = [ "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.12.1", "lazy_static", "lazycell", "log", + "peeking_take_while", "prettyplease", "proc-macro2", "quote", @@ -2261,9 +2261,9 @@ dependencies = [ [[package]] name = "oqs" -version = "0.10.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "828f06d734c93f9ba75f0ae8075a7eb25d71c59705f2a7bf426ed997fe62beb5" +checksum = "311616b64e55f9dc110286ea7cc5e475216dc332c54291e59a709c32fddb3a14" dependencies = [ "cstr_core", "libc", @@ -2272,9 +2272,9 @@ dependencies = [ [[package]] name = "oqs-sys" -version = "0.10.1+liboqs-0.12.0" +version = "0.9.1+liboqs-0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4084714b15c8545e7dd2e9d1e4a5482547ece7476d07f4b5e96b5babb78c4dd" +checksum = "afa79adc3c10f8e01d0b134c159254e5843ec3f91c0bd868e57777beb3329e17" dependencies = [ "bindgen", "build-deps", @@ -2340,6 +2340,12 @@ dependencies = [ "hmac", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.3.1" diff --git a/Cargo.toml b/Cargo.toml index 1e028f3f3..41811ceef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -196,7 +196,7 @@ num-derive = "0.4" num-traits = { version = "0.2.18", default-features = false } num_enum = "0.7.3" openssl = "0.10.72" -oqs = { version = "0.10", default-features = false, features = ["std", "falcon", "vendored"] } +oqs = { version = "0.9", default-features = false, features = ["std", "falcon", "vendored"] } pairing = "0.23.0" parking_lot = "0.12" pbkdf2 = { version = "0.11.0", default-features = false } From 986091e7816bf48fae705b970f8b3ab1d30890a9 Mon Sep 17 00:00:00 2001 From: zz-sol Date: Wed, 28 Jan 2026 10:43:14 -0500 Subject: [PATCH 7/8] fix CI --- Cargo.lock | 15 +++------------ Cargo.toml | 1 + falcon-signature/src/error.rs | 1 + falcon-signature/src/instruction.rs | 2 +- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 565eb93fb..2f3f88e4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1616,11 +1616,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.12" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5256,15 +5256,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-targets" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 41811ceef..722da0ee6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,6 +178,7 @@ five8 = "1.0.0" five8_const = "1.0.0" getrandom = "0.3.4" group = "0.13.0" +home = "0.5.9" hex = "0.4.3" hmac = "0.12.1" im = "15.1.0" diff --git a/falcon-signature/src/error.rs b/falcon-signature/src/error.rs index 431cc291f..b8883fcb8 100644 --- a/falcon-signature/src/error.rs +++ b/falcon-signature/src/error.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "std")] use crate::constants::{ MAX_SIGNATURE_SIZE, MIN_SIGNATURE_SIZE, PUBKEY_HEADER, PUBKEY_SIZE, SIGNATURE_HEADER, }; diff --git a/falcon-signature/src/instruction.rs b/falcon-signature/src/instruction.rs index 10b906632..fc74882f2 100644 --- a/falcon-signature/src/instruction.rs +++ b/falcon-signature/src/instruction.rs @@ -1,5 +1,5 @@ #[cfg(not(feature = "std"))] -use alloc::vec::Vec; +use alloc::{vec, vec::Vec}; use { crate::{ constants::{DATA_START, PUBKEY_SIZE, SIGNATURE_OFFSETS_SIZE, SIGNATURE_OFFSETS_START}, From 8a23c31534d20bc819d79e5d3196a93aa5fe5569 Mon Sep 17 00:00:00 2001 From: zz-sol Date: Sun, 1 Feb 2026 10:26:37 -0500 Subject: [PATCH 8/8] fix CI --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 722da0ee6..b14d01e33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,9 +178,9 @@ five8 = "1.0.0" five8_const = "1.0.0" getrandom = "0.3.4" group = "0.13.0" -home = "0.5.9" hex = "0.4.3" hmac = "0.12.1" +home = "0.5.9" im = "15.1.0" indicatif = "0.17.9" itertools = "0.12.1"