VYPR
High severity7.5NVD Advisory· Published Apr 22, 2026· Updated Apr 24, 2026

CVE-2026-34065

CVE-2026-34065

Description

nimiq-primitives contains primitives (e.g., block, account, transaction) to be used in Nimiq's Rust implementation. Prior to version 1.3.0, an untrusted p2p peer can cause a node to panic by announcing an election macro block whose validators set contains an invalid compressed BLS voting key. Hashing an election macro header hashes validators and reaches Validators::voting_keys(), which calls validator.voting_key.uncompress().unwrap() and panics on invalid bytes. The patch for this vulnerability is included as part of v1.3.0. No known workarounds are available.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
nimiq-primitivescrates.io
<= 0.2.0

Affected products

1

Patches

1
e10eaebcd777

Fix crash via invalid election macro block validators voting key

https://github.com/nimiq/core-rs-albatrossJose Daniel HernandezMar 27, 2026via ghsa
7 files changed · +133 16
  • consensus/src/sync/live/block_queue/queue.rs+10 0 modified
    @@ -160,6 +160,16 @@ impl<N: Network> BlockQueue<N> {
     
             let blockchain = self.blockchain.read();
     
    +        // Validate header before hashing to reject malformed blocks (e.g. invalid BLS keys)
    +        // that would panic during hash computation.
    +        if block
    +            .verify_header(blockchain.network_id(), block.is_skip())
    +            .is_err()
    +        {
    +            block_source.reject_block(&self.network);
    +            return None;
    +        }
    +
             let block_hash = block.hash_cached();
             let block_number = block.block_number();
             let head_height = blockchain.block_number();
    
  • genesis-builder/src/lib.rs+6 1 modified
    @@ -663,7 +663,12 @@ impl GenesisBuilder {
             {
                 let decompressed_path = directory.join("decompressed_keys.dat");
                 let mut decompressed = Vec::new();
    -            for key in block.validators().expect("must be election").voting_keys() {
    +            for key in block
    +                .validators()
    +                .expect("must be election")
    +                .voting_keys()
    +                .expect("genesis validators must have valid keys")
    +            {
                     decompressed.extend_from_slice(&key.trusted_serialize());
                 }
                 fs::write(decompressed_path, decompressed)?;
    
  • primitives/block/src/equivocation_proof.rs+6 3 modified
    @@ -558,12 +558,15 @@ impl DoubleVoteProof {
                     // Verify the signatures.
                     let mut agg_pk = AggregatePublicKey::new();
                     for slot in signers {
    -                    let pk = validators
    +                    if let Some(pk) = validators
                             .get_validator_by_slot_number(*slot)
                             .voting_key
                             .uncompress()
    -                        .expect("Failed to uncompress CompressedPublicKey");
    -                    agg_pk.aggregate(pk);
    +                    {
    +                        agg_pk.aggregate(pk);
    +                    } else {
    +                        return Err(EquivocationProofError::InvalidJustification);
    +                    }
                     }
                     if !agg_pk.verify(&message, signature) {
                         return Err(EquivocationProofError::InvalidJustification);
    
  • primitives/block/src/macro_block.rs+49 1 modified
    @@ -244,6 +244,12 @@ impl MacroHeader {
             if self.is_election() != self.validators.is_some() {
                 return Err(BlockError::InvalidValidators);
             }
    +        // Validate that all BLS voting keys can be decompressed and structural invariants hold.
    +        if let Some(ref validators) = self.validators {
    +            validators
    +                .validate_keys()
    +                .map_err(|_| BlockError::InvalidValidators)?;
    +        }
             Ok(())
         }
     
    @@ -394,7 +400,14 @@ pub enum IntoSlotsError {
     
     #[cfg(test)]
     mod test {
    -    use super::MacroBlock;
    +    use nimiq_bls::CompressedPublicKey as BlsCompressedPublicKey;
    +    use nimiq_keys::Ed25519PublicKey as SchnorrPublicKey;
    +    use nimiq_primitives::{
    +        policy::Policy,
    +        slots_allocation::{Validator, Validators},
    +    };
    +
    +    use super::{MacroBlock, MacroHeader};
     
         #[test]
         fn size_well_below_msg_limit() {
    @@ -404,4 +417,39 @@ mod test {
                     <= dbg!(nimiq_network_interface::network::MIN_SUPPORTED_MSG_SIZE)
             );
         }
    +
    +    /// Verifies that invalid BLS voting keys are caught by verify() and
    +    /// voting_keys(), preventing the panic that would occur in hash_cached().
    +    ///
    +    /// Regression test for: untrusted peer announces an election macro block
    +    /// with an invalid BLS voting key. Before the fix, `hash_cached()` would
    +    /// panic via `voting_keys()` → `uncompress().unwrap()`.
    +    #[test]
    +    fn invalid_bls_voting_key_rejected_by_verify() {
    +        let invalid_compressed = BlsCompressedPublicKey {
    +            public_key: [0xFF; BlsCompressedPublicKey::SIZE],
    +        };
    +
    +        let validator = Validator::new(
    +            nimiq_keys::Address::default(),
    +            invalid_compressed,
    +            SchnorrPublicKey::default(),
    +            0..Policy::SLOTS,
    +        );
    +
    +        let validators = Validators::new(vec![validator]);
    +
    +        // validate_keys() catches the invalid key.
    +        assert!(validators.validate_keys().is_err());
    +
    +        // voting_keys() returns an error instead of panicking.
    +        assert!(validators.voting_keys().is_err());
    +
    +        // MacroHeader::verify() rejects the block.
    +        let header = MacroHeader {
    +            validators: Some(validators),
    +            ..Default::default()
    +        };
    +        assert!(header.verify().is_err());
    +    }
     }
    
  • primitives/block/src/tendermint.rs+6 3 modified
    @@ -68,12 +68,15 @@ impl TendermintProof {
             let mut agg_pk = AggregatePublicKey::new();
     
             for slot in signer_slots {
    -            let pk = current_validators
    +            if let Some(pk) = current_validators
                     .get_validator_by_slot_number(slot)
                     .voting_key
                     .uncompress()
    -                .expect("Failed to uncompress CompressedPublicKey");
    -            agg_pk.aggregate(pk);
    +            {
    +                agg_pk.aggregate(pk);
    +            } else {
    +                return false;
    +            }
             }
     
             // Verify the aggregated signature against our aggregated public key.
    
  • primitives/src/slots_allocation.rs+53 7 modified
    @@ -14,7 +14,7 @@
     //!                      |             SlotBand                      |    SlotBand       |
     //!                      +-------------------------------------------+-------------------+
     //! ```
    -use std::{cmp::Ordering, collections::BTreeMap, ops::Range, slice::Iter};
    +use std::{cmp::Ordering, collections::BTreeMap, fmt, ops::Range, slice::Iter};
     
     use ark_ec::CurveGroup;
     use ark_serialize::CanonicalSerialize;
    @@ -26,6 +26,18 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
     
     use crate::{merkle_tree::merkle_tree_construct, policy::Policy};
     
    +/// Error returned when a validator has an invalid BLS voting key that cannot be decompressed.
    +#[derive(Clone, Debug)]
    +pub struct InvalidValidatorsError;
    +
    +impl fmt::Display for InvalidValidatorsError {
    +    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    +        write!(f, "Invalid BLS voting key for validator")
    +    }
    +}
    +
    +impl std::error::Error for InvalidValidatorsError {}
    +
     /// This is the depth of the PKTree circuit.
     pub const PK_TREE_DEPTH: usize = 5;
     /// This is the number of leaves in the PKTree circuit.
    @@ -187,20 +199,52 @@ impl Validators {
         }
     
         /// Returns the G2 projective associated with each slot, in order.
    -    pub fn voting_keys_g2(&self) -> Vec<G2Projective> {
    -        self.voting_keys().iter().map(|pk| pk.public_key).collect()
    +    pub fn voting_keys_g2(&self) -> Result<Vec<G2Projective>, InvalidValidatorsError> {
    +        Ok(self.voting_keys()?.iter().map(|pk| pk.public_key).collect())
         }
     
         /// Returns the voting key associated with each slot, in order.
    -    pub fn voting_keys(&self) -> Vec<BlsPublicKey> {
    +    pub fn voting_keys(&self) -> Result<Vec<BlsPublicKey>, InvalidValidatorsError> {
             let mut pks = vec![];
     
             for validator in self.iter() {
    -            let pk = *validator.voting_key.uncompress().unwrap();
    +            let pk = *validator
    +                .voting_key
    +                .uncompress()
    +                .ok_or(InvalidValidatorsError)?;
                 pks.append(&mut vec![pk; validator.num_slots() as usize]);
             }
     
    -        pks
    +        Ok(pks)
    +    }
    +
    +    /// Checks that all validator voting keys can be decompressed and that the
    +    /// validator set structure is valid (correct number of slots, compatible with ZK circuit).
    +    pub fn validate_keys(&self) -> Result<(), InvalidValidatorsError> {
    +        let mut total_slots: u16 = 0;
    +
    +        // Check that all keys can be decompressed and count total slots in one pass
    +        for validator in self.iter() {
    +            validator
    +                .voting_key
    +                .uncompress()
    +                .ok_or(InvalidValidatorsError)?;
    +            total_slots += validator.num_slots();
    +        }
    +
    +        // Check structural invariants that would cause panics in Hash implementation
    +
    +        // Must have exactly Policy::SLOTS total slots
    +        if total_slots != Policy::SLOTS {
    +            return Err(InvalidValidatorsError);
    +        }
    +
    +        // Must be a multiple of PK_TREE_BREADTH for ZK circuit compatibility
    +        if !(total_slots as usize).is_multiple_of(PK_TREE_BREADTH) {
    +            return Err(InvalidValidatorsError);
    +        }
    +
    +        Ok(())
         }
     
         /// Iterates over the validators.
    @@ -213,7 +257,9 @@ impl Hash for Validators {
         /// This function is meant to calculate the public key tree "off-circuit". Generating the public key
         /// tree with this function guarantees that it is compatible with the ZK circuit.
         fn hash<H: HashOutput>(&self) -> H {
    -        let public_keys = self.voting_keys_g2();
    +        let public_keys = self.voting_keys_g2().expect(
    +            "BLS voting keys must be valid — call validate_keys() before hashing untrusted data",
    +        );
     
             // Checking that the number of public keys is equal to the number of validator slots.
             assert_eq!(public_keys.len(), Policy::SLOTS as usize);
    
  • zkp/src/proof_system/prove.rs+3 1 modified
    @@ -668,7 +668,9 @@ fn prove_macro_block<R: CryptoRng + Rng>(
         let prev_validators = prev_block
             .get_validators()
             .ok_or(NanoZKPError::InvalidBlock)?;
    -    let prev_pks = prev_validators.voting_keys_g2();
    +    let prev_pks = prev_validators
    +        .voting_keys_g2()
    +        .map_err(|_| NanoZKPError::InvalidBlock)?;
     
         // Calculate final public key tree root.
         let signer_bitmap: Vec<bool> = final_block
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.