summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorStuart Stock <stuart@int08h.com>2019-02-10 18:41:44 -0600
committerStuart Stock <stuart@int08h.com>2019-02-10 18:41:44 -0600
commit177372f63a33b25a075c4cba446319929950b0f8 (patch)
tree5f274bf2f4874a6dc66770c71cf1a036432f122f /src
parent62e6dd64a276dbf8e10cbe6a4111f0cfb98d163f (diff)
downloadroughenough-177372f63a33b25a075c4cba446319929950b0f8.zip
initial version of client response mangling
Diffstat (limited to 'src')
-rw-r--r--src/config/mod.rs13
-rw-r--r--src/grease.rs94
-rw-r--r--src/kms/envelope.rs6
-rw-r--r--src/lib.rs5
-rw-r--r--src/message.rs13
-rw-r--r--src/server.rs13
-rw-r--r--src/stats/aggregated.rs2
-rw-r--r--src/stats/mod.rs4
-rw-r--r--src/stats/per_client.rs8
9 files changed, 137 insertions, 21 deletions
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 70da38e..1d1d149 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -22,20 +22,20 @@
//! such as files or environment variables.
//!
+mod environment;
+mod file;
+mod memory;
+
use std::net::SocketAddr;
use std::time::Duration;
-mod file;
-pub use self::file::FileConfig;
-
-mod environment;
pub use self::environment::EnvironmentConfig;
-
-mod memory;
+pub use self::file::FileConfig;
pub use self::memory::MemoryConfig;
use crate::key::KmsProtection;
use crate::Error;
+use crate::SEED_LENGTH;
/// Maximum number of requests to process in one batch and include the the Merkle tree.
pub const DEFAULT_BATCH_SIZE: u8 = 64;
@@ -172,6 +172,7 @@ pub fn is_valid_config(cfg: &Box<dyn ServerConfig>) -> bool {
);
is_valid = false;
}
+
if cfg.fault_percentage() > 50 {
error!("fault_percentage {} is invalid; valid range 0-50", cfg.fault_percentage());
is_valid = false;
diff --git a/src/grease.rs b/src/grease.rs
new file mode 100644
index 0000000..d7f80a2
--- /dev/null
+++ b/src/grease.rs
@@ -0,0 +1,94 @@
+// Copyright 2017-2019 int08h LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//!
+//! Adds deliberate errors to client responses.
+//!
+
+use rand::{FromEntropy, Rng};
+use rand::rngs::SmallRng;
+use rand::distributions::{Bernoulli, Distribution};
+use rand::seq::SliceRandom;
+
+use crate::RtMessage;
+use crate::grease::Pathologies::*;
+
+enum Pathologies {
+ RandomlyOrderTags,
+ CorruptResponseSignature,
+ // TODO(stuart) semantic pathologies
+}
+
+static ALL_PATHOLOGIES: &[Pathologies] = &[
+ RandomlyOrderTags,
+ CorruptResponseSignature
+];
+
+pub struct Grease {
+ enabled: bool,
+ dist: Bernoulli,
+ prng: SmallRng,
+}
+
+impl Grease {
+ pub fn new(fault_percentage: u8) -> Self {
+ Grease {
+ enabled: fault_percentage > 0,
+ dist: Bernoulli::from_ratio(u32::from(fault_percentage), 100),
+ prng: SmallRng::from_entropy(),
+ }
+ }
+
+ #[inline]
+ pub fn should_add_error(&mut self) -> bool {
+ if self.enabled { self.prng.sample(self.dist) } else { false }
+ }
+
+ pub fn add_errors(&mut self, src_msg: &RtMessage) -> RtMessage {
+ match ALL_PATHOLOGIES.choose(&mut self.prng) {
+ Some(CorruptResponseSignature) => src_msg.to_owned(),
+ Some(RandomlyOrderTags) => src_msg.to_owned(),
+ None => unreachable!()
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::grease::Grease;
+ use crate::RtMessage;
+
+ #[test]
+ fn verify_error_probability() {
+ const TRIALS: u64 = 100_000;
+ const TOLERANCE: f64 = 0.75;
+
+ for target in 1..50 {
+ let mut g = Grease::new(target);
+ let (lower, upper) = (target as f64 - TOLERANCE, target as f64 + TOLERANCE);
+
+ let acc: u64 = (0..TRIALS)
+ .map(|_| if g.should_add_error() { 1 } else { 0 })
+ .sum();
+
+ let percentage = 100.0 * (acc as f64 / TRIALS as f64);
+
+ assert_eq!(
+ percentage > lower && percentage < upper,
+ true,
+ "target {}, actual {} [{}, {}]", target, percentage, lower, upper
+ );
+ }
+ }
+}
diff --git a/src/kms/envelope.rs b/src/kms/envelope.rs
index b82e61d..d83aa64 100644
--- a/src/kms/envelope.rs
+++ b/src/kms/envelope.rs
@@ -17,7 +17,7 @@ use std::io::{Cursor, Read, Write};
use ring::aead::{open_in_place, seal_in_place, OpeningKey, SealingKey, AES_256_GCM};
use ring::rand::{SecureRandom, SystemRandom};
-use crate::MIN_SEED_LENGTH;
+use crate::SEED_LENGTH;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use crate::kms::{KmsError, KmsProvider, AD, DEK_SIZE_BYTES, NONCE_SIZE_BYTES, TAG_SIZE_BYTES};
@@ -33,7 +33,7 @@ const MIN_PAYLOAD_SIZE: usize = DEK_LEN_FIELD
+ NONCE_LEN_FIELD
+ DEK_SIZE_BYTES
+ NONCE_SIZE_BYTES
- + MIN_SEED_LENGTH as usize
+ + SEED_LENGTH as usize
+ TAG_SIZE_BYTES;
// No input prefix to skip, consume entire buffer
@@ -41,7 +41,7 @@ const IN_PREFIX_LEN: usize = 0;
// Convenience function to create zero-filled Vec of given size
fn vec_zero_filled(len: usize) -> Vec<u8> {
- (0..len).into_iter().map(|_| 0).collect()
+ (0..len).map(|_| 0).collect()
}
/// Envelope encryption of the long-term key seed value.
diff --git a/src/lib.rs b/src/lib.rs
index 7e8dbe7..4ec3955 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -63,6 +63,7 @@ mod message;
mod tag;
pub mod config;
+pub mod grease;
pub mod key;
pub mod kms;
pub mod merkle;
@@ -95,8 +96,8 @@ pub fn roughenough_version() -> String {
/// Minimum size (in bytes) of a client request
pub const MIN_REQUEST_LENGTH: u32 = 1024;
-/// Minimum size (in bytes) of seeds used to derive private keys
-pub const MIN_SEED_LENGTH: u32 = 32;
+/// Size (in bytes) of seeds used to derive private keys
+pub const SEED_LENGTH: u32 = 32;
/// Size (in bytes) of an Ed25519 public key
pub const PUBKEY_LENGTH: u32 = 32;
diff --git a/src/message.rs b/src/message.rs
index d598dee..fea3014 100644
--- a/src/message.rs
+++ b/src/message.rs
@@ -69,6 +69,19 @@ impl RtMessage {
}
}
+ ///
+ /// Dangerous: construct a new RtMessage **without validation or error checking**.
+ ///
+ /// Intended _only_ for construction of deliberately bogus responses as part of [Roughtime's
+ /// ecosystem](https://roughtime.googlesource.com/roughtime/+/HEAD/ECOSYSTEM.md#maintaining-a-healthy-software-ecosystem).
+ ///
+ pub fn new_deliberately_invalid(tags: Vec<Tag>, values: Vec<Vec<u8>>) -> Self {
+ RtMessage {
+ tags,
+ values,
+ }
+ }
+
/// Internal function to create a single tag message
fn single_tag_message(bytes: &[u8], msg: &mut Cursor<&[u8]>) -> Result<Self, Error> {
if bytes.len() < 8 {
diff --git a/src/server.rs b/src/server.rs
index 85411ce..8d2d7c1 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -37,6 +37,7 @@ use mio_extras::timer::Timer;
use crate::{Error, RtMessage, Tag, MIN_REQUEST_LENGTH};
use crate::config::ServerConfig;
+use crate::grease::Grease;
use crate::key::{LongTermKey, OnlineKey};
use crate::kms;
use crate::merkle::MerkleTree;
@@ -78,6 +79,7 @@ pub struct Server {
health_listener: Option<TcpListener>,
keep_running: Arc<AtomicBool>,
poll_duration: Option<Duration>,
+ grease: Grease,
timer: Timer<()>,
poll: Poll,
merkle: MerkleTree,
@@ -154,6 +156,7 @@ impl Server {
Box::new(AggregatedStats::new())
};
+ let grease = Grease::new(config.fault_percentage());
let merkle = MerkleTree::new();
let requests = Vec::with_capacity(config.batch_size() as usize);
@@ -167,6 +170,7 @@ impl Server {
keep_running,
poll_duration,
+ grease,
timer,
poll,
merkle,
@@ -357,7 +361,7 @@ impl Server {
}
}
- fn send_responses(&mut self) -> () {
+ fn send_responses(&mut self) {
let merkle_root = self.merkle.compute_root();
// The SREP tag is identical for each response
@@ -365,8 +369,11 @@ impl Server {
for (i, &(ref nonce, ref src_addr)) in self.requests.iter().enumerate() {
let paths = self.merkle.get_paths(i);
- let resp = self.make_response(&srep, &self.cert_bytes, &paths, i as u32);
- let resp_bytes = resp.encode().unwrap();
+ let resp_msg = {
+ let r = self.make_response(&srep, &self.cert_bytes, &paths, i as u32);
+ if self.grease.should_add_error() { self.grease.add_errors(&r) } else { r }
+ };
+ let resp_bytes = resp_msg.encode().unwrap();
let bytes_sent = self
.socket
diff --git a/src/stats/aggregated.rs b/src/stats/aggregated.rs
index afff109..389a99d 100644
--- a/src/stats/aggregated.rs
+++ b/src/stats/aggregated.rs
@@ -91,7 +91,7 @@ impl ServerStats for AggregatedStats {
0
}
- fn get_client_stats(&self, _addr: &IpAddr) -> Option<&ClientStatEntry> {
+ fn stats_for_client(&self, _addr: &IpAddr) -> Option<&ClientStatEntry> {
None
}
diff --git a/src/stats/mod.rs b/src/stats/mod.rs
index 29e7ad8..ad90bd2 100644
--- a/src/stats/mod.rs
+++ b/src/stats/mod.rs
@@ -74,7 +74,7 @@ pub trait ServerStats {
fn total_unique_clients(&self) -> u64;
- fn get_client_stats(&self, addr: &IpAddr) -> Option<&ClientStatEntry>;
+ fn stats_for_client(&self, addr: &IpAddr) -> Option<&ClientStatEntry>;
fn iter(&self) -> Iter<IpAddr, ClientStatEntry>;
@@ -131,7 +131,7 @@ mod test {
stats.add_response(&ip, 2048);
stats.add_response(&ip, 1024);
- let entry = stats.get_client_stats(&ip).unwrap();
+ let entry = stats.stats_for_client(&ip).unwrap();
assert_eq!(entry.valid_requests, 1);
assert_eq!(entry.invalid_requests, 0);
assert_eq!(entry.responses_sent, 2);
diff --git a/src/stats/per_client.rs b/src/stats/per_client.rs
index 0799a76..8641fde 100644
--- a/src/stats/per_client.rs
+++ b/src/stats/per_client.rs
@@ -40,7 +40,7 @@ impl PerClientStats {
pub fn new() -> Self {
PerClientStats {
- clients: HashMap::with_capacity(128),
+ clients: HashMap::with_capacity(64),
num_overflows: 0,
max_clients: PerClientStats::MAX_CLIENTS,
}
@@ -50,7 +50,7 @@ impl PerClientStats {
#[cfg(test)]
pub fn with_limit(limit: usize) -> Self {
PerClientStats {
- clients: HashMap::with_capacity(128),
+ clients: HashMap::with_capacity(64),
num_overflows: 0,
max_clients: limit,
}
@@ -64,7 +64,7 @@ impl PerClientStats {
self.num_overflows += 1;
}
- return too_big;
+ too_big
}
#[allow(dead_code)]
@@ -155,7 +155,7 @@ impl ServerStats for PerClientStats {
self.clients.len() as u64
}
- fn get_client_stats(&self, addr: &IpAddr) -> Option<&ClientStatEntry> {
+ fn stats_for_client(&self, addr: &IpAddr) -> Option<&ClientStatEntry> {
self.clients.get(addr)
}