Security: Avoid silently corrupting invalid times during serialization (#2149)

* Security: panic if an internally generated time is out of range

If Zebra has a bug where it generates blocks, transactions, or meta
addresses with bad times, panic. This avoids sending bad data onto the
network.

(Previously, Zebra would truncate some of these times, silently
corrupting the underlying data.)

Make it clear that deserialization of these objects is infalliable.
This commit is contained in:
teor 2021-05-18 06:53:10 +10:00 committed by GitHub
parent b0b8b2f61a
commit b600e82d6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 26 additions and 15 deletions

View File

@ -27,10 +27,12 @@ impl ZcashSerialize for Header {
self.previous_block_hash.zcash_serialize(&mut writer)?; self.previous_block_hash.zcash_serialize(&mut writer)?;
writer.write_all(&self.merkle_root.0[..])?; writer.write_all(&self.merkle_root.0[..])?;
writer.write_all(&self.commitment_bytes[..])?; writer.write_all(&self.commitment_bytes[..])?;
// this is a truncating cast, rather than a saturating cast writer.write_u32::<LittleEndian>(
// but u32 times are valid until 2106, and our block verification time self.time
// checks should detect any truncation. .timestamp()
writer.write_u32::<LittleEndian>(self.time.timestamp() as u32)?; .try_into()
.expect("deserialized and generated timestamps are u32 values"),
)?;
writer.write_u32::<LittleEndian>(self.difficulty_threshold.0)?; writer.write_u32::<LittleEndian>(self.difficulty_threshold.0)?;
writer.write_all(&self.nonce[..])?; writer.write_all(&self.nonce[..])?;
self.solution.zcash_serialize(&mut writer)?; self.solution.zcash_serialize(&mut writer)?;
@ -76,7 +78,7 @@ impl ZcashDeserialize for Header {
merkle_root: merkle::Root(reader.read_32_bytes()?), merkle_root: merkle::Root(reader.read_32_bytes()?),
commitment_bytes: reader.read_32_bytes()?, commitment_bytes: reader.read_32_bytes()?,
// This can't panic, because all u32 values are valid `Utc.timestamp`s // This can't panic, because all u32 values are valid `Utc.timestamp`s
time: Utc.timestamp(reader.read_u32::<LittleEndian>()? as i64, 0), time: Utc.timestamp(reader.read_u32::<LittleEndian>()?.into(), 0),
difficulty_threshold: CompactDifficulty(reader.read_u32::<LittleEndian>()?), difficulty_threshold: CompactDifficulty(reader.read_u32::<LittleEndian>()?),
nonce: reader.read_32_bytes()?, nonce: reader.read_32_bytes()?,
solution: equihash::Solution::zcash_deserialize(reader)?, solution: equihash::Solution::zcash_deserialize(reader)?,

View File

@ -1,4 +1,4 @@
use std::io; use std::{convert::TryInto, io};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use chrono::{DateTime, TimeZone, Utc}; use chrono::{DateTime, TimeZone, Utc};
@ -65,7 +65,8 @@ impl ZcashSerialize for LockTime {
// we can always compute a hash of a transaction object. // we can always compute a hash of a transaction object.
match self { match self {
LockTime::Height(block::Height(n)) => writer.write_u32::<LittleEndian>(*n)?, LockTime::Height(block::Height(n)) => writer.write_u32::<LittleEndian>(*n)?,
LockTime::Time(t) => writer.write_u32::<LittleEndian>(t.timestamp() as u32)?, LockTime::Time(t) => writer
.write_u32::<LittleEndian>(t.timestamp().try_into().expect("time is in range"))?,
} }
Ok(()) Ok(())
} }
@ -77,7 +78,8 @@ impl ZcashDeserialize for LockTime {
if n <= block::Height::MAX.0 { if n <= block::Height::MAX.0 {
Ok(LockTime::Height(block::Height(n))) Ok(LockTime::Height(block::Height(n)))
} else { } else {
Ok(LockTime::Time(Utc.timestamp(n as i64, 0))) // This can't panic, because all u32 values are valid `Utc.timestamp`s
Ok(LockTime::Time(Utc.timestamp(n.into(), 0)))
} }
} }
} }

View File

@ -2,6 +2,7 @@
use std::{ use std::{
cmp::{Ord, Ordering}, cmp::{Ord, Ordering},
convert::TryInto,
io::{Read, Write}, io::{Read, Write},
net::SocketAddr, net::SocketAddr,
}; };
@ -309,7 +310,12 @@ impl PartialOrd for MetaAddr {
impl ZcashSerialize for MetaAddr { impl ZcashSerialize for MetaAddr {
fn zcash_serialize<W: Write>(&self, mut writer: W) -> Result<(), std::io::Error> { fn zcash_serialize<W: Write>(&self, mut writer: W) -> Result<(), std::io::Error> {
writer.write_u32::<LittleEndian>(self.get_last_seen().timestamp() as u32)?; writer.write_u32::<LittleEndian>(
self.get_last_seen()
.timestamp()
.try_into()
.expect("time is in range"),
)?;
writer.write_u64::<LittleEndian>(self.services.bits())?; writer.write_u64::<LittleEndian>(self.services.bits())?;
writer.write_socket_addr(self.addr)?; writer.write_socket_addr(self.addr)?;
Ok(()) Ok(())
@ -318,7 +324,8 @@ impl ZcashSerialize for MetaAddr {
impl ZcashDeserialize for MetaAddr { impl ZcashDeserialize for MetaAddr {
fn zcash_deserialize<R: Read>(mut reader: R) -> Result<Self, SerializationError> { fn zcash_deserialize<R: Read>(mut reader: R) -> Result<Self, SerializationError> {
let last_seen = Utc.timestamp(reader.read_u32::<LittleEndian>()? as i64, 0); // This can't panic, because all u32 values are valid `Utc.timestamp`s
let last_seen = Utc.timestamp(reader.read_u32::<LittleEndian>()?.into(), 0);
let services = PeerServices::from_bits_truncate(reader.read_u64::<LittleEndian>()?); let services = PeerServices::from_bits_truncate(reader.read_u64::<LittleEndian>()?);
let addr = reader.read_socket_addr()?; let addr = reader.read_socket_addr()?;

View File

@ -2,7 +2,8 @@ use proptest::{arbitrary::any, arbitrary::Arbitrary, prelude::*};
use super::{MetaAddr, PeerAddrState, PeerServices}; use super::{MetaAddr, PeerAddrState, PeerServices};
use std::{net::SocketAddr, time::SystemTime}; use chrono::{TimeZone, Utc};
use std::net::SocketAddr;
impl Arbitrary for MetaAddr { impl Arbitrary for MetaAddr {
type Parameters = (); type Parameters = ();
@ -11,16 +12,15 @@ impl Arbitrary for MetaAddr {
( (
any::<SocketAddr>(), any::<SocketAddr>(),
any::<PeerServices>(), any::<PeerServices>(),
any::<SystemTime>(), any::<u32>(),
any::<PeerAddrState>(), any::<PeerAddrState>(),
) )
.prop_map( .prop_map(
|(addr, services, last_seen, last_connection_state)| MetaAddr { |(addr, services, last_seen, last_connection_state)| MetaAddr {
addr, addr,
services, services,
// TODO: implement constraints on last_seen as part of the // This can't panic, because all u32 values are valid `Utc.timestamp`s
// last_connection_state refactor in #1849 last_seen: Utc.timestamp(last_seen.into(), 0),
last_seen: last_seen.into(),
last_connection_state, last_connection_state,
}, },
) )