Skip to content

Commit 5c32f31

Browse files
Merge pull request from GHSA-xqqc-c5gw-c5r5
* Add the is_matching_chain_id() predicate * Add TrustedBlockState::chain_id field * Implement matching chain-id check in verifier * Add test * Add test for verifier * Bump light client verifier and dependent crates to v0.28.0-pre.1 Signed-off-by: Thane Thomson <connect@thanethomson.com> * Bump kvstore test light client dependency Signed-off-by: Thane Thomson <connect@thanethomson.com> * Bump version to v0.28.0 Signed-off-by: Thane Thomson <connect@thanethomson.com> * Add changelog entries for security fix Signed-off-by: Thane Thomson <connect@thanethomson.com> * Prepare v0.28.0 release changelog Signed-off-by: Thane Thomson <connect@thanethomson.com> * Rebuild changelog Signed-off-by: Thane Thomson <connect@thanethomson.com> * Update release date in changelog Signed-off-by: Thane Thomson <connect@thanethomson.com> Signed-off-by: Thane Thomson <connect@thanethomson.com> Co-authored-by: Thane Thomson <connect@thanethomson.com>
1 parent 60d003b commit 5c32f31

30 files changed

+210
-41
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- `[tendermint-light-client-verifier]` Add `is_matching_chain_id`
2+
method to the `VerificationPredicates` trait
3+
([#1249](https://github.com/informalsystems/tendermint-rs/pull/1249))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- `[tendermint-light-client-verifier]` Add a
2+
`chain_id` field to the `TrustedBlockState` struct
3+
([#1249](https://github.com/informalsystems/tendermint-rs/pull/1249))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- `[tendermint-light-client]` Fix an issue where the light client was not
2+
checking that the chain ID of the trusted and untrusted headers match
3+
([#1249](https://github.com/informalsystems/tendermint-rs/pull/1249))

.changelog/v0.28.0/summary.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
*Dec 13, 2022*
2+
3+
This is primarily a security-related release, and although it's a breaking
4+
release, the breaking changes are relatively minor.
5+
6+
It is highly recommended that all tendermint-rs light client users upgrade to
7+
this version immediately.

CHANGELOG.md

+30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
# CHANGELOG
22

3+
## v0.28.0
4+
5+
*Dec 13, 2022*
6+
7+
This is primarily a security-related release, and although it's a breaking
8+
release, the breaking changes are relatively minor.
9+
10+
It is highly recommended that all tendermint-rs light client users upgrade to
11+
this version immediately.
12+
13+
### BREAKING
14+
15+
- `[tendermint-light-client-verifier]` Add a
16+
`chain_id` field to the `TrustedBlockState` struct
17+
([#1249](https://github.com/informalsystems/tendermint-rs/pull/1249))
18+
- `[tendermint-light-client-verifier]` Add `is_matching_chain_id`
19+
method to the `VerificationPredicates` trait
20+
([#1249](https://github.com/informalsystems/tendermint-rs/pull/1249))
21+
22+
### IMPROVEMENTS
23+
24+
- `[tendermint-light-client-js]` Switch to serde-wasm-bindgen for marshalling
25+
JS values ([#1242](https://github.com/informalsystems/tendermint-rs/pull/1242))
26+
27+
### SECURITY
28+
29+
- `[tendermint-light-client]` Fix an issue where the light client was not
30+
checking that the chain ID of the trusted and untrusted headers match
31+
([#1249](https://github.com/informalsystems/tendermint-rs/pull/1249))
32+
333
## v0.27.0
434

535
*Nov 28, 2022*

abci/Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tendermint-abci"
3-
version = "0.27.0"
3+
version = "0.28.0"
44
authors = ["Informal Systems <hello@informal.systems>"]
55
edition = "2018"
66
license = "Apache-2.0"
@@ -33,7 +33,7 @@ binary = [
3333
[dependencies]
3434
bytes = { version = "1.0", default-features = false }
3535
prost = { version = "0.11", default-features = false }
36-
tendermint-proto = { version = "0.27.0", default-features = false, path = "../proto" }
36+
tendermint-proto = { version = "0.28.0", default-features = false, path = "../proto" }
3737
tracing = { version = "0.1", default-features = false }
3838
flex-error = { version = "0.4.4", default-features = false }
3939
structopt = { version = "0.3", optional = true, default-features = false }

config/Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tendermint-config"
3-
version = "0.27.0" # Also update `html_root_url` in lib.rs and
3+
version = "0.28.0" # Also update `html_root_url` in lib.rs and
44
# depending crates (rpc, light-node, ..) when bumping this
55
license = "Apache-2.0"
66
homepage = "https://www.tendermint.com/"
@@ -25,7 +25,7 @@ all-features = true
2525
rustdoc-args = ["--cfg", "docsrs"]
2626

2727
[dependencies]
28-
tendermint = { version = "0.27.0", default-features = false, path = "../tendermint" }
28+
tendermint = { version = "0.28.0", default-features = false, path = "../tendermint" }
2929
flex-error = { version = "0.4.4", default-features = false }
3030
serde = { version = "1", features = ["derive"] }
3131
serde_json = "1"

light-client-js/Cargo.toml

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tendermint-light-client-js"
3-
version = "0.27.0"
3+
version = "0.28.0"
44
authors = ["Informal Systems <hello@informal.systems>"]
55
edition = "2018"
66
license = "Apache-2.0"
@@ -22,8 +22,8 @@ default = ["console_error_panic_hook"]
2222
[dependencies]
2323
serde = { version = "1.0", default-features = false, features = [ "derive" ] }
2424
serde_json = { version = "1.0", default-features = false }
25-
tendermint = { version = "0.27.0", default-features = false, path = "../tendermint" }
26-
tendermint-light-client-verifier = { version = "0.27.0", default-features = false, path = "../light-client-verifier" }
25+
tendermint = { version = "0.28.0", default-features = false, path = "../tendermint" }
26+
tendermint-light-client-verifier = { version = "0.28.0", default-features = false, path = "../light-client-verifier" }
2727
wasm-bindgen = { version = "0.2.63", default-features = false, features = [ "serde-serialize" ] }
2828
serde-wasm-bindgen = { version = "0.4.5", default-features = false }
2929

light-client-verifier/Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tendermint-light-client-verifier"
3-
version = "0.27.0"
3+
version = "0.28.0"
44
edition = "2021"
55
license = "Apache-2.0"
66
readme = "README.md"
@@ -26,7 +26,7 @@ rustdoc-args = ["--cfg", "docsrs"]
2626
default = ["flex-error/std", "flex-error/eyre_tracer"]
2727

2828
[dependencies]
29-
tendermint = { version = "0.27.0", path = "../tendermint", default-features = false }
29+
tendermint = { version = "0.28.0", path = "../tendermint", default-features = false }
3030

3131
derive_more = { version = "0.99.5", default-features = false, features = ["display"] }
3232
serde = { version = "1.0.106", default-features = false }

light-client-verifier/src/errors.rs

+10
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ define_error! {
112112
e.got, e.expected)
113113
},
114114

115+
ChainIdMismatch
116+
{
117+
got: String,
118+
expected: String,
119+
}
120+
| e | {
121+
format_args!("chain-id mismatch: got={0} expected={1}",
122+
e.got, e.expected)
123+
},
124+
115125
NonMonotonicBftTime
116126
{
117127
header_bft_time: Time,

light-client-verifier/src/predicates.rs

+41-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use core::time::Duration;
44

5-
use tendermint::{block::Height, hash::Hash};
5+
use tendermint::{block::Height, chain::Id as ChainId, hash::Hash};
66

77
use crate::{
88
errors::VerificationError,
@@ -160,6 +160,22 @@ pub trait VerificationPredicates: Send + Sync {
160160
}
161161
}
162162

163+
/// Check that the chain-ids of the trusted header and the untrusted one are the same
164+
fn is_matching_chain_id(
165+
&self,
166+
untrusted_chain_id: &ChainId,
167+
trusted_chain_id: &ChainId,
168+
) -> Result<(), VerificationError> {
169+
if untrusted_chain_id == trusted_chain_id {
170+
Ok(())
171+
} else {
172+
Err(VerificationError::chain_id_mismatch(
173+
untrusted_chain_id.to_string(),
174+
trusted_chain_id.to_string(),
175+
))
176+
}
177+
}
178+
163179
/// Check that there is enough validators overlap between the trusted validator set
164180
/// and the untrusted signed header.
165181
fn has_sufficient_validators_overlap(
@@ -282,6 +298,30 @@ mod tests {
282298
}
283299
}
284300

301+
#[test]
302+
fn test_is_matching_chain_id() {
303+
let val = vec![Validator::new("val-1")];
304+
let header_one = Header::new(&val).chain_id("chaina-1").generate().unwrap();
305+
let header_two = Header::new(&val).chain_id("chainb-1").generate().unwrap();
306+
307+
let vp = ProdPredicates::default();
308+
309+
// 1. ensure valid header verifies
310+
let result_ok = vp.is_matching_chain_id(&header_one.chain_id, &header_one.chain_id);
311+
assert!(result_ok.is_ok());
312+
313+
// 2. ensure header with different chain-id fails
314+
let result_err = vp.is_matching_chain_id(&header_one.chain_id, &header_two.chain_id);
315+
316+
match result_err {
317+
Err(VerificationError(VerificationErrorDetail::ChainIdMismatch(e), _)) => {
318+
assert_eq!(e.got, header_one.chain_id.to_string());
319+
assert_eq!(e.expected, header_two.chain_id.to_string());
320+
},
321+
_ => panic!("expected ChainIdMismatch error"),
322+
}
323+
}
324+
285325
#[test]
286326
fn test_is_within_trust_period() {
287327
let val = Validator::new("val-1");

light-client-verifier/src/types.rs

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use tendermint::{
88
header::Header as TMHeader, signed_header::SignedHeader as TMSignedHeader,
99
Commit as TMCommit,
1010
},
11+
chain::Id as ChainId,
1112
trust_threshold::TrustThresholdFraction,
1213
validator::{Info as TMValidatorInfo, Set as TMValidatorSet},
1314
};
@@ -78,6 +79,7 @@ impl Status {
7879

7980
/// Trusted block parameters needed for light client verification.
8081
pub struct TrustedBlockState<'a> {
82+
pub chain_id: &'a ChainId,
8183
pub header_time: Time,
8284
pub height: Height,
8385
pub next_validators: &'a ValidatorSet,
@@ -143,6 +145,7 @@ impl LightBlock {
143145
/// trusted state.
144146
pub fn as_trusted_state(&self) -> TrustedBlockState<'_> {
145147
TrustedBlockState {
148+
chain_id: &self.signed_header.header.chain_id,
146149
header_time: self.signed_header.header.time,
147150
height: self.signed_header.header.height,
148151
next_validators: &self.next_validators,

light-client-verifier/src/verifier.rs

+70
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@ where
195195
.predicates
196196
.is_monotonic_bft_time(untrusted.signed_header.header.time, trusted.header_time));
197197

198+
// Check that the chain-id of the untrusted block matches that of the trusted state
199+
verdict!(self
200+
.predicates
201+
.is_matching_chain_id(&untrusted.signed_header.header.chain_id, trusted.chain_id));
202+
198203
let trusted_next_height = trusted.height.increment();
199204

200205
if untrusted.height() == trusted_next_height {
@@ -287,3 +292,68 @@ where
287292
/// The default production implementation of the [`PredicateVerifier`].
288293
pub type ProdVerifier =
289294
PredicateVerifier<ProdPredicates, ProdVotingPowerCalculator, ProdCommitValidator, ProdHasher>;
295+
296+
#[cfg(test)]
297+
mod tests {
298+
use alloc::{borrow::ToOwned, string::ToString};
299+
use core::{ops::Sub, time::Duration};
300+
301+
use tendermint::Time;
302+
use tendermint_testgen::{light_block::LightBlock as TestgenLightBlock, Generator};
303+
304+
use crate::{
305+
errors::VerificationErrorDetail, options::Options, types::LightBlock, ProdVerifier,
306+
Verdict, Verifier,
307+
};
308+
309+
#[test]
310+
fn test_verification_failure_on_chain_id_mismatch() {
311+
let now = Time::now();
312+
313+
// Create a default light block with a valid chain-id for height `1` with a timestamp 20
314+
// secs before now (to be treated as trusted state)
315+
let light_block_1: LightBlock = TestgenLightBlock::new_default_with_time_and_chain_id(
316+
"chain-1".to_owned(),
317+
now.sub(Duration::from_secs(20)).unwrap(),
318+
1u64,
319+
)
320+
.generate()
321+
.unwrap()
322+
.into();
323+
324+
// Create another default block with a different chain-id for height `2` with a timestamp 10
325+
// secs before now (to be treated as untrusted state)
326+
let light_block_2: LightBlock = TestgenLightBlock::new_default_with_time_and_chain_id(
327+
"forged-chain".to_owned(),
328+
now.sub(Duration::from_secs(10)).unwrap(),
329+
2u64,
330+
)
331+
.generate()
332+
.unwrap()
333+
.into();
334+
335+
let vp = ProdVerifier::default();
336+
let opt = Options {
337+
trust_threshold: Default::default(),
338+
trusting_period: Duration::from_secs(60),
339+
clock_drift: Default::default(),
340+
};
341+
342+
let verdict = vp.verify(
343+
light_block_2.as_untrusted_state(),
344+
light_block_1.as_trusted_state(),
345+
&opt,
346+
Time::now(),
347+
);
348+
349+
match verdict {
350+
Verdict::Invalid(VerificationErrorDetail::ChainIdMismatch(e)) => {
351+
let chain_id_1 = light_block_1.signed_header.header.chain_id;
352+
let chain_id_2 = light_block_2.signed_header.header.chain_id;
353+
assert_eq!(e.got, chain_id_2.to_string());
354+
assert_eq!(e.expected, chain_id_1.to_string());
355+
},
356+
v => panic!("expected ChainIdMismatch error, got: {:?}", v),
357+
}
358+
}
359+
}

light-client/Cargo.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tendermint-light-client"
3-
version = "0.27.0"
3+
version = "0.28.0"
44
edition = "2018"
55
license = "Apache-2.0"
66
readme = "README.md"
@@ -34,9 +34,9 @@ unstable = []
3434
mbt = []
3535

3636
[dependencies]
37-
tendermint = { version = "0.27.0", path = "../tendermint", default-features = false }
38-
tendermint-rpc = { version = "0.27.0", path = "../rpc", default-features = false }
39-
tendermint-light-client-verifier = { version = "0.27.0", path = "../light-client-verifier", default-features = false }
37+
tendermint = { version = "0.28.0", path = "../tendermint", default-features = false }
38+
tendermint-rpc = { version = "0.28.0", path = "../rpc", default-features = false }
39+
tendermint-light-client-verifier = { version = "0.28.0", path = "../light-client-verifier", default-features = false }
4040

4141
contracts = { version = "0.6.2", default-features = false }
4242
crossbeam-channel = { version = "0.4.2", default-features = false }

light-client/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
nonstandard_style
1010
)]
1111
#![doc(
12-
html_root_url = "https://docs.rs/tendermint-light-client/0.27.0",
12+
html_root_url = "https://docs.rs/tendermint-light-client/0.28.0",
1313
html_logo_url = "https://raw.githubusercontent.com/informalsystems/tendermint-rs/master/img/logo-tendermint-rs_3961x4001.png"
1414
)]
1515
#![cfg_attr(docsrs, feature(doc_cfg))]

p2p/Cargo.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tendermint-p2p"
3-
version = "0.27.0"
3+
version = "0.28.0"
44
edition = "2018"
55
license = "Apache-2.0"
66
repository = "https://github.com/informalsystems/tendermint-rs"
@@ -44,9 +44,9 @@ aead = { version = "0.4.1", default-features = false }
4444
flex-error = { version = "0.4.4", default-features = false }
4545

4646
# path dependencies
47-
tendermint = { path = "../tendermint", version = "0.27.0", default-features = false }
48-
tendermint-proto = { path = "../proto", version = "0.27.0", default-features = false }
49-
tendermint-std-ext = { path = "../std-ext", version = "0.27.0", default-features = false }
47+
tendermint = { path = "../tendermint", version = "0.28.0", default-features = false }
48+
tendermint-proto = { path = "../proto", version = "0.28.0", default-features = false }
49+
tendermint-std-ext = { path = "../std-ext", version = "0.28.0", default-features = false }
5050

5151
# optional dependencies
5252
prost-derive = { version = "0.11", optional = true }

p2p/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
unused_qualifications
2121
)]
2222
#![doc(
23-
html_root_url = "https://docs.rs/tendermint-p2p/0.27.0",
23+
html_root_url = "https://docs.rs/tendermint-p2p/0.28.0",
2424
html_logo_url = "https://raw.githubusercontent.com/informalsystems/tendermint-rs/master/img/logo-tendermint-rs_3961x4001.png"
2525
)]
2626

pbt-gen/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tendermint-pbt-gen"
3-
version = "0.27.0"
3+
version = "0.28.0"
44
authors = ["Informal Systems <hello@informal.systems>"]
55
edition = "2018"
66
license = "Apache-2.0"

proto/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tendermint-proto"
3-
version = "0.27.0"
3+
version = "0.28.0"
44
authors = ["Informal Systems <hello@informal.systems>"]
55
edition = "2018"
66
license = "Apache-2.0"

proto/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#![deny(warnings, trivial_casts, trivial_numeric_casts, unused_import_braces)]
55
#![allow(clippy::large_enum_variant)]
66
#![forbid(unsafe_code)]
7-
#![doc(html_root_url = "https://docs.rs/tendermint-proto/0.27.0")]
7+
#![doc(html_root_url = "https://docs.rs/tendermint-proto/0.28.0")]
88

99
extern crate alloc;
1010

0 commit comments

Comments
 (0)