Repository: cloudflare/cfnts Branch: master Commit: fa53b9844c36 Files: 69 Total size: 180.5 KB Directory structure: gitextract_mvp4c08z/ ├── .cargo/ │ └── config ├── .github/ │ └── workflows/ │ └── cfntsci.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.toml ├── Dockerfile.cfnts ├── Dockerfile.memcache ├── LICENSE ├── Makefile ├── README.md ├── RELEASE_NOTES ├── docker-compose.yaml ├── scripts/ │ ├── fill-memcached.py │ ├── run_client.sh │ ├── run_memcached.sh │ └── run_server.sh ├── src/ │ ├── cfsock.rs │ ├── cmd.rs │ ├── cookie.rs │ ├── error.rs │ ├── key_rotator.rs │ ├── main.rs │ ├── metrics.rs │ ├── ntp/ │ │ ├── client.rs │ │ ├── mod.rs │ │ ├── protocol.rs │ │ └── server/ │ │ ├── config.rs │ │ ├── mod.rs │ │ └── ntp_server.rs │ ├── nts_ke/ │ │ ├── client.rs │ │ ├── mod.rs │ │ ├── records/ │ │ │ ├── aead_algorithm.rs │ │ │ ├── end_of_message.rs │ │ │ ├── error.rs │ │ │ ├── mod.rs │ │ │ ├── new_cookie.rs │ │ │ ├── next_protocol.rs │ │ │ ├── port.rs │ │ │ ├── server.rs │ │ │ └── warning.rs │ │ └── server/ │ │ ├── config.rs │ │ ├── connection.rs │ │ ├── ke_server.rs │ │ ├── listener.rs │ │ └── mod.rs │ └── sub_command/ │ ├── client.rs │ ├── ke_server.rs │ ├── mod.rs │ └── ntp_server.rs └── tests/ ├── ca-key.pem ├── ca.csr ├── ca.pem ├── chain.pem ├── cookie.key ├── generate.sh ├── int-config.json ├── intermediate-key.pem ├── intermediate.csr ├── intermediate.json ├── intermediate.pem ├── ntp-config.yaml ├── ntp-upstream-config.yaml ├── nts-ke-config.yaml ├── test-config.json ├── test.json ├── tls-key.pem ├── tls-pkcs8.pem ├── tls.csr └── tls.pem ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config ================================================ [build] rustflags = ["-Ctarget-feature=+aes,+ssse3"] rustdocflags = ["-Ctarget-feature=+aes,+ssse3"] [test] rustflags = ["-Ctarget-feature=+aes,+ssse3"] ================================================ FILE: .github/workflows/cfntsci.yml ================================================ --- name: cfntsCI on: push: branches: - master pull_request: jobs: Testing: runs-on: ubuntu-latest steps: - name: Checking out uses: actions/checkout@v3 - name: Setting up Rust uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable components: clippy, rustfmt override: true - name: Rust cache uses: Swatinem/rust-cache@v1 - name: Linting run: cargo clippy --all-targets -- -D warnings - name: Format run: cargo fmt --all --check - name: Building run: cargo build --release - name: Testing run: cargo test -- --nocapture E2E: runs-on: ubuntu-latest steps: - name: Checking out uses: actions/checkout@v3 - name: Run integration tests uses: isbang/compose-action@v1.4.1 with: compose-file: "./docker-compose.yaml" up-flags: "--build --abort-on-container-exit --exit-code-from client" ================================================ FILE: .gitignore ================================================ /target **/*.rs.bk **/*.swp ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing We welcome your contributions. Note that your contributions must be licensed under the BSD-style license found in LICENSE. To make our lives as well as yours easier please indicate when a PR is a work in progress vs. ready for review. ================================================ FILE: Cargo.toml ================================================ [package] name = "cfnts" version = "2019.6.0" authors = [ "Watson Ladd ", "Gabbi Fisher ", "Tanya Verma ", "Suphanat Chunhapanya ", ] edition = "2018" [dependencies] byteorder = "1.3.2" # Used for command-line parsing and validation. clap = "2.33.0" config = "0.9.3" crossbeam = "0.7.3" lazy_static = "1.4.0" libc = "0.2.65" log = "0.4.8" memcache = "0.13.1" mio = "0.6.19" miscreant = "0.4.2" socket2 = "0.4.7" nix = "0.13.0" prometheus = "0.7.0" rand = "0.7.2" ring = "0.16.9" rustls = "0.16.0" simple_logger = "1.3.0" # More advanced logging system than `log`. slog = { version = "2.5.2", features = [ "max_level_trace", "release_max_level_debug", ]} # We configure at runtime # Add scopes to the logging system. slog-scope = "4.3.0" # Used for fowarding all the `log` crate logging to `slog_scope::logger()`. slog-stdlog = "~4.0.0" # A wrapper of `slog` to make logging more convenient. If you want to increase a version here, # please make sure that `TerminalLoggerBuilder::build` doesn't return an error. sloggers = "=0.3.4" webpki = "0.21.0" webpki-roots = "0.18.0" ================================================ FILE: Dockerfile.cfnts ================================================ FROM rust:1.69.0-bookworm as builder COPY src src COPY .cargo .cargo COPY Cargo.toml Cargo.lock ./ RUN cargo build --release FROM debian:bookworm COPY --from=builder ./target/release/cfnts ./target/release/cfnts ================================================ FILE: Dockerfile.memcache ================================================ FROM debian:bookworm RUN apt-get update && \ apt-get -y install memcached python3-memcache ================================================ FILE: LICENSE ================================================ Copyright (c) 2019, Cloudflare. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Makefile ================================================ SHELL=/bin/bash TARGET_ARCHS ?= x86_64-unknown-linux-gnu release: @git diff --quiet || { echo "Run in a clean repo"; exit 1; } cargo bump $(shell cfsetup release next-tag) cargo update git add Cargo.toml Cargo.lock git commit -m "Bump version in Cargo.toml to release tag" cfsetup release update cf-package: for TARGET_ARCH in $(TARGET_ARCHS); do \ echo $$TARGET_ARCH && \ cargo deb --target $$TARGET_ARCH && \ mv target/$$TARGET_ARCH/debian/*.deb ./ || \ exit 1; \ done ================================================ FILE: README.md ================================================ # cfnts ## DEPRECATION NOTICE **This software is no longer maintained. Consider using an alternative NTS implementation such as [chrony](https://chrony-project.org) or [ntpd-rs](https://github.com/pendulum-project/ntpd-rs).** cfnts is an implementation of the NTS protocol written in Rust. **Prereqs**: Rust **Building**: We use cargo to build the software. `docker-compose up` will spawn several Docker containers that run tests. **Running** Run the NTS client using `./target/release/cfnts client [--4 | --6] [-p ] [-c ] [-n ] ` Default port is `4460`. Using `-4` forces the use of ipv4 for all connections to the server, and using `-6` forces the use of ipv6. These two arguments are mutually exclusive. If neither of them is used, then the client will use whichever one is supported by the server (preference for ipv6 if supported). To run a server you will need a memcached compatible server, together with a script based on fill-memcached.py that will write a new random key into /nts/nts-keys/ every hour and delete old ones. Then you can run the ntp server and the nts server. This split and use of memcached exists to enable deployments where a small dedicated device serves NTP, while a bigger server carries out the key exchange. **Examples**: 1. `./target/release/cfnts client time.cloudflare.com` 2. `./target/release/cfnts client kong.rellim.com -p 123` ================================================ FILE: RELEASE_NOTES ================================================ 2019.6.0 - 2019-06-04 CRYPTO-1040: Conform with HTTP 1.1 in metrics - 2019-06-04 Bump version in Cargo.toml to release tag 2019.5.2 - 2019-05-31 CRYPTO-1016: Test with chain, avoid races with logger - 2019-05-31 Bump version in Cargo.toml to release tag 2019.5.1 - 2019-05-29 CRYPTO-1007: Do not respond to non client mode packets - 2019-05-29 CRYPTO-1006: Expose version in metrics - 2019-05-30 Fix makefile to make release - 2019-05-30 Bump version in Cargo.toml to release tag 2019.5.0 - 2019-02-26 Initial Commit - 2019-02-26 Functioning Server - 2019-04-08 Vendored deps - 2019-04-08 Change config to use the vendored sources - 2019-04-09 Starting point from NTS Hackathon - 2019-04-09 Tell client what port to use - 2019-04-09 Ensure the port is in our test config - 2019-04-10 Change over to mio and import prometheus for metrics - 2019-04-10 Silence warnings - 2019-04-18 Undo type magic required by tokio - 2019-04-18 Add logging and more error messages/codes. Also handle blocking. - 2019-04-18 Include vendor changes - 2019-04-12 Key rotation and cfsetup compose execution - 2019-04-19 UDP portion - 2019-04-23 Serve metrics - 2019-04-24 Switch to slog for logging - 2019-05-03 Various improvements - 2019-05-06 CRYPTO-924: Smaller cookies - 2019-05-09 CRYPTO-940: Change log level at runtime - 2019-05-09 CRYPTO-922 Build debian packages - 2019-05-08 Support specification of multiple listening addressess - 2019-04-29 Implement connection to upstream process - 2019-05-10 Use kernel timestamping for more accurate timing - 2019-05-06 CRYPTO-891: Timeouts for nts_ke connections - 2019-05-28 Use correct nonce length according to RFC 5116 - 2019-05-22 CRYPTO-957/CRYPTO-979: set socket options on our listening sockets - 2019-05-24 CRYPTO-986 Use TLS 1.3 only for NTS - 2019-05-23 CRYPTO-960 Better handling of configuration errors ================================================ FILE: docker-compose.yaml ================================================ version: "3.8" services: server: build: context: . dockerfile: Dockerfile.cfnts depends_on: - memcache volumes: - ./tests:/tests - ./scripts:/scripts entrypoint: ["/scripts/run_server.sh"] client: build: context: . dockerfile: Dockerfile.cfnts depends_on: - server volumes: - ./tests:/tests - ./scripts:/scripts entrypoint: ["/scripts/run_client.sh"] memcache: build: context: . dockerfile: Dockerfile.memcache volumes: - ./scripts:/scripts entrypoint: ["/scripts/run_memcached.sh"] ================================================ FILE: scripts/fill-memcached.py ================================================ import memcache import time import math print("filling memcache") servers = ["localhost:11211"] mc = memcache.Client(servers) rand = open("/dev/urandom", "rb") interval = 3600 now = int(math.floor(time.time())) for i in range(-50, 4): epoch = int((math.floor(now/interval)+i)*interval) key = "/nts/nts-keys/%s"%epoch mc.set(key, rand.read(16)) ================================================ FILE: scripts/run_client.sh ================================================ #!/bin/bash # Retry for 10 times. for i in $(seq 1 10); do if ./target/release/cfnts client server -c tests/ca.pem; then exit 0 else echo "The server is unavailable - sleeping" sleep 1 fi done exit 1 ================================================ FILE: scripts/run_memcached.sh ================================================ #!/bin/bash echo "Running memcache" date "+%s" memcached -u root & sleep 2 python3 scripts/fill-memcached.py echo "done" wait $! ================================================ FILE: scripts/run_server.sh ================================================ #!/bin/bash sleep 5 date "+%s" RUST_BACKTRACE=1 ./target/release/cfnts ke-server -f tests/nts-ke-config.yaml & RUST_BACKTRACE=1 ./target/release/cfnts ntp-server -f tests/ntp-upstream-config.yaml & RUST_BACKTRACE=1 ./target/release/cfnts ntp-server -f tests/ntp-config.yaml ================================================ FILE: src/cfsock.rs ================================================ use libc::*; use socket2::{Domain, Socket, Type}; use std::net::SocketAddr; use std::os::unix::io::AsRawFd; #[cfg(target_os = "linux")] fn set_freebind(fd: c_int) -> Result<(), std::io::Error> { use std::io::{Error, ErrorKind}; const IP_FREEBIND: libc::c_int = 0xf; match unsafe { setsockopt( fd, SOL_IP, IP_FREEBIND, &1u32 as *const u32 as *const c_void, std::mem::size_of::() as u32, ) } { -1 => Err(std::io::Error::new( ErrorKind::Other, Error::last_os_error(), )), _ => Ok(()), } } #[cfg(not(target_os = "linux"))] fn set_freebind(_fd: c_int) -> Result<(), std::io::Error> { Ok(()) // no op for mac build } pub fn tcp_listener(addr: &SocketAddr) -> Result { let domain = match addr { SocketAddr::V4(..) => Domain::IPV4, SocketAddr::V6(..) => Domain::IPV6, }; let socket = Socket::new(domain, Type::STREAM, None)?; socket.set_reuse_address(true)?; set_freebind(socket.as_raw_fd())?; socket.bind(&(*addr).into())?; socket.listen(128)?; Ok(socket.into()) } pub fn udp_listen(addr: &SocketAddr) -> Result { let domain = match addr { SocketAddr::V4(..) => Domain::IPV4, SocketAddr::V6(..) => Domain::IPV6, }; let socket = Socket::new(domain, Type::DGRAM, None)?; socket.set_reuse_address(true)?; set_freebind(socket.as_raw_fd())?; socket.bind(&(*addr).into())?; Ok(socket.into()) } ================================================ FILE: src/cmd.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! Command line argument definitions and validations. use clap::{App, Arg, SubCommand}; /// Create the subcommand `client`. fn create_clap_client_subcommand<'a, 'b>() -> App<'a, 'b> { // Arguments for `client` subcommand. let args = [ // The hostname is always required and will immediately // follow the subcommand string. Arg::with_name("host") .index(1) .required(true) .help("NTS server's hostname (do not include port)"), // The rest will be passed as unrequired command-line options. Arg::with_name("port") .long("port") .short("p") .takes_value(true) .required(false) .help("Specifies NTS server's port. The default port number is 4460."), Arg::with_name("cert") .long("cert") .short("c") .takes_value(true) .required(false) .help("Specifies a path to the trusted certificate in PEM format."), Arg::with_name("ipv4") .long("ipv4") .short("4") .conflicts_with("ipv6") .help("Forces use of IPv4 only"), Arg::with_name("ipv6") .long("ipv6") .short("6") .conflicts_with("ipv4") .help("Forces use of IPv6 only"), ]; // Create a new subcommand. SubCommand::with_name("client") .about("Initiates an NTS connection with the remote server") .args(&args) } /// Create the subcommand `ke-server`. fn create_clap_ke_server_subcommand<'a, 'b>() -> App<'a, 'b> { // Arguments for `ke-server` subcommand. let args = [Arg::with_name("configfile") .long("file") .short("f") .takes_value(true) .required(false) .help( "Specifies a path to the configuration file. If the path is not specified, \ the system-wide configuration file (/etc/cfnts/ke-server.config) will be \ used instead", )]; // Create a new subcommand. SubCommand::with_name("ke-server") .about("Runs NTS-KE server over TLS/TCP") .args(&args) } /// Create the subcommand `ntp-server`. fn create_clap_ntp_server_subcommand<'a, 'b>() -> App<'a, 'b> { // Arguments for `ntp-server` subcommand. let args = [Arg::with_name("configfile") .long("file") .short("f") .takes_value(true) .required(false) .help( "Specifies a path to the configuration file. If the path is not specified, \ the system-wide configuration file (/etc/cfnts/ntp-server.config) will be \ used instead", )]; // Create a new subcommand. SubCommand::with_name("ntp-server") .about("Interfaces with NTP using UDP") .args(&args) } /// Create the whole command-line configuration. pub fn create_clap_command() -> App<'static, 'static> { App::new(env!("CARGO_PKG_NAME")) .about(env!("CARGO_PKG_DESCRIPTION")) .version(env!("CARGO_PKG_VERSION")) .arg( Arg::with_name("debug") .long("debug") .short("d") .help("Turns on debug logging"), ) .subcommands(vec![ // List of all available subcommands. create_clap_client_subcommand(), create_clap_ke_server_subcommand(), create_clap_ntp_server_subcommand(), ]) } ================================================ FILE: src/cookie.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. use miscreant::aead; use miscreant::aead::Aead; use rand::Rng; use std::convert::TryInto; use std::fs::File; use std::io; use std::io::Read; use crate::key_rotator::KeyId; pub const COOKIE_SIZE: usize = 100; #[derive(Debug, Copy, Clone)] pub struct NTSKeys { pub c2s: [u8; 32], pub s2c: [u8; 32], } /// Cookie key. #[derive(Clone, Debug)] pub struct CookieKey(Vec); impl CookieKey { /// Parse a cookie key from a file. /// /// # Errors /// /// There will be an error, if we cannot open the file. /// pub fn parse(filename: &str) -> Result { let mut file = File::open(filename)?; let mut buffer = Vec::new(); file.read_to_end(&mut buffer)?; Ok(CookieKey(buffer)) } /// Return a byte slice of a cookie key content. pub fn as_bytes(&self) -> &[u8] { self.0.as_slice() } } // Only used in test. #[cfg(test)] impl From<&[u8]> for CookieKey { fn from(bytes: &[u8]) -> CookieKey { CookieKey(Vec::from(bytes)) } } pub fn make_cookie(keys: NTSKeys, master_key: &[u8], key_id: KeyId) -> Vec { let mut nonce = [0; 16]; rand::thread_rng().fill(&mut nonce); let mut plaintext = [0; 64]; plaintext[..32].copy_from_slice(&keys.c2s[..32]); plaintext[32..64].copy_from_slice(&keys.s2c[..32]); let mut aead = aead::Aes128SivAead::new(master_key); let mut ciphertext = aead.seal(&nonce, &[], &plaintext); let mut out = Vec::new(); out.extend(&key_id.to_be_bytes()); out.extend(&nonce); out.append(&mut ciphertext); out } pub fn get_keyid(cookie: &[u8]) -> Option { if cookie.len() < 4 { None } else { Some(KeyId::from_be_bytes((&cookie[0..4]).try_into().unwrap())) } } fn unpack(pt: Vec) -> Option { if pt.len() != 64 { None } else { let mut key = NTSKeys { c2s: [0; 32], s2c: [0; 32], }; key.c2s[..32].copy_from_slice(&pt[..32]); key.s2c[..32].copy_from_slice(&pt[32..64]); Some(key) } } pub fn eat_cookie(cookie: &[u8], key: &[u8]) -> Option { if cookie.len() < 40 { return None; } let ciphertext = &cookie[4..]; let mut aead = aead::Aes128SivAead::new(key); let answer = aead.open(&ciphertext[0..16], &[], &ciphertext[16..]); match answer { Err(_) => None, Ok(buf) => unpack(buf), } } #[cfg(test)] mod tests { use super::*; fn check_eq(a: NTSKeys, b: NTSKeys) { for i in 0..32 { assert_eq!(a.c2s[i], b.c2s[i]); assert_eq!(a.s2c[i], b.s2c[i]); } } #[test] fn check_cookie() { let test = NTSKeys { s2c: [9; 32], c2s: [10; 32], }; let master_key = [0x07; 32]; let key_id = KeyId::from_be_bytes([0x03; 4]); let mut cookie = make_cookie(test, &master_key, key_id); assert_eq!(cookie.len(), COOKIE_SIZE); assert_eq!(get_keyid(&cookie).unwrap(), key_id); check_eq(eat_cookie(&cookie, &master_key).unwrap(), test); cookie[9] = 0xff; cookie[10] = 0xff; assert!(eat_cookie(&cookie, &master_key).is_none()); } } ================================================ FILE: src/error.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! Traits for working with errors. use std::error::Error; /// `WrapError` allows the implementor to wrap its own error type in another error type. pub trait WrapError { /// The returned type in case that the result has no error. type Item; /// Wrapping an error in the error type `T`. fn wrap_err(self) -> Result; } /// Trait implementation for `config::ConfigError`. // The reason that we have a lifetime bound 'static is that we want T to either contain no lifetime // parameter or contain only the 'static lifetime parameter. impl WrapError for Result where T: 'static + Error + Send + Sync, { /// Don't change the returned type, in case there is no error. type Item = S; fn wrap_err(self) -> Result { self.map_err(|error| config::ConfigError::Foreign(Box::new(error))) } } /// Trait implementation for `std::io::Error`. // The reason that we have a lifetime bound 'static is that we want T to either contain no lifetime // parameter or contain only the 'static lifetime parameter. impl WrapError for Result where T: 'static + Error + Send + Sync, { /// Don't change the returned type, in case there is no error. type Item = S; fn wrap_err(self) -> Result { self.map_err(|error| std::io::Error::new(std::io::ErrorKind::Other, error)) } } ================================================ FILE: src/key_rotator.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! Key rotator implementation, which provides key synchronization with Memcached server. use lazy_static::lazy_static; #[cfg(not(test))] use memcache::MemcacheError; use prometheus::{opts, register_counter, register_int_counter, IntCounter}; use ring::hmac; use std::collections::HashMap; use std::sync::{Arc, RwLock}; use std::thread; #[cfg(not(test))] use std::time::SystemTime; use std::time::{Duration, UNIX_EPOCH}; use crate::cookie::CookieKey; lazy_static! { static ref ROTATION_COUNTER: IntCounter = register_int_counter!("ntp_key_rotations_total", "Number of key rotations").unwrap(); static ref FAILURE_COUNTER: IntCounter = register_int_counter!( "ntp_key_rotations_failed_total", "Number of failures in key rotation" ) .unwrap(); } /// Key id for `KeyRotator`. // This struct should be `Clone` and `Copy` because the internal representation is just a `u32`. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct KeyId(u32); impl KeyId { /// Create `KeyId` from raw `u32`. pub fn new(key_id: u32) -> KeyId { KeyId(key_id) } /// Create `KeyId` from a `u64` epoch. The 32 most significant bits of the parameter will be /// discarded. pub fn from_epoch(epoch: u64) -> KeyId { // This will discard the 32 most significant bits. let epoch_residue = epoch as u32; KeyId(epoch_residue) } /// Create `KeyId` from its representation as a byte array in big endian. pub fn from_be_bytes(bytes: [u8; 4]) -> KeyId { KeyId(u32::from_be_bytes(bytes)) } /// Return the memory representation of this `KeyId` as a byte array in big endian. pub fn to_be_bytes(self) -> [u8; 4] { self.0.to_be_bytes() } } /// Error struct returned from `KeyRotator::rotate` method. #[derive(Debug)] pub enum RotateError { /// Error from Memcached server. MemcacheError(MemcacheError), /// Error when the Memcached server doesn't have a specified `KeyId`. KeyIdNotFound(KeyId), } impl From for RotateError { /// Wrap MemcacheError. fn from(error: MemcacheError) -> RotateError { RotateError::MemcacheError(error) } } /// Key rotator. pub struct KeyRotator { /// URL of the Memcached server. memcached_url: String, /// Prefix for the Memcached key. prefix: String, // This property type needs to fit an Epoch time in seconds. /// Length of each period in seconds. duration: u64, // The number of forward and backward periods are `u64` because the timestamp is `u64` and the // duration can be as small as 1. /// The number of future periods that the rotator must cache their values from the /// Memcached server. number_of_forward_periods: u64, /// The number of previous periods that the rotator must cache their values from the /// Memcached server. number_of_backward_periods: u64, /// Cookie key that will be used as a MAC key of the rotator. master_key: CookieKey, /// Key id of the current period. latest_key_id: KeyId, /// Cache store. cache: HashMap, /// Logger. // TODO: since we don't use the logger now, I will put an `allow(dead_code)` here first. I will // remove it when it's used. #[allow(dead_code)] logger: slog::Logger, } impl KeyRotator { /// Connect to the Memcached server and sync some inital keys. pub fn connect( prefix: String, memcached_url: String, master_key: CookieKey, logger: slog::Logger, ) -> Result { let mut rotator = KeyRotator { // Zero shouldn't be a valid KeyId. This is just a temporary value. latest_key_id: KeyId::new(0), // The cache should never be empty. This is just a temporary value. cache: HashMap::new(), // It seems that currently we don't have to customize the following three properties, // so I will just put default values. duration: 3600, number_of_forward_periods: 2, number_of_backward_periods: 24, // From parameters. prefix, memcached_url, master_key, logger, }; // Maximum number of times that we want to try rotating the keys. let maximum_try = 5; // Try to rotate the keys up to 5 times to make sure that the rotator has some keys in it. // If it doesn't, we will not have any key to use. for try_number in 1.. { match rotator.rotate() { Err(error) => { // Side-effect. Logging. // Disable the log for now because the Error trait is not implemented for // RotateError yet. // error!(rotator.logger, "failure to initialize key rotation: {}", error); // If it already tried a lot of times already, it may be a time to give up. if try_number == maximum_try { return Err(error); } // Wait for 5 seconds before retrying key rotation. std::thread::sleep(std::time::Duration::from_secs(5)); } // If it's a success, stop retrying. Ok(()) => break, } } Ok(rotator) } /// Rotate keys. /// /// # Panics /// /// If the system time is before the UNIX Epoch time. /// /// # Errors /// /// There is an error, if there is a connection problem with Memcached server or the Memcached /// server doesn't contain a key id it supposed to contain. /// pub fn rotate(&mut self) -> Result<(), RotateError> { // Side-effect. It's not related to the operation. ROTATION_COUNTER.inc(); let duration = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("The system time must be after the UNIX Epoch time."); // The number of seconds since the Epoch time. let timestamp = duration.as_secs(); // The current period number of the timestamp. let current_period = timestamp / self.duration; // The timestamp at the beginning of the current period. let current_epoch = current_period * self.duration; // The first period number that we want to iterate through. let first_period = current_period.saturating_sub(self.number_of_backward_periods); // The last period number that we want to iterate through. let last_period = current_period.saturating_add(self.number_of_forward_periods); let removed_period = first_period.saturating_sub(1); let removed_epoch = removed_period * self.duration; self.cache_remove(KeyId::from_epoch(removed_epoch)); // Connecting to memcached. I have to add [..] because it seems that Rust is not smart // enough to do auto-dereference. let mut client = memcache::Client::connect(&self.memcached_url[..])?; for period_number in first_period..=last_period { // The timestamp at the beginning of the period. let epoch = period_number * self.duration; let memcached_key = format!("{}/{}", self.prefix, epoch); let memcached_value: Option> = client.get(&memcached_key)?; let key_id = KeyId::from_epoch(epoch); match memcached_value { Some(value) => self.cache_insert(key_id, value.as_slice()), None => { FAILURE_COUNTER.inc(); return Err(RotateError::KeyIdNotFound(key_id)); } } } // Not all of our friends may have gotten the same forwards keys as we did. self.latest_key_id = KeyId::from_epoch(current_epoch); Ok(()) } /// Add an entry to the cache. // It should be private. Don't make it public. fn cache_insert(&mut self, key_id: KeyId, value: &[u8]) { // Create a MAC key. let mac_key = hmac::Key::new(hmac::HMAC_SHA256, self.master_key.as_bytes()); // Generating a MAC tag with a MAC key. let tag = hmac::sign(&mac_key, value); self.cache.insert(key_id, tag); } /// Remove an entry from the cache. // It should be private. Don't make it public. fn cache_remove(&mut self, key_id: KeyId) { self.cache.remove(&key_id); } /// Return the latest key id and hmac tag of the rotator. pub fn latest_key_value(&self) -> (KeyId, &hmac::Tag) { // This unwrap cannot panic because the HashMap will always contain the latest key id. (self.latest_key_id, self.get(self.latest_key_id).unwrap()) } /// Return an entry in the cache using a key id. pub fn get(&self, key_id: KeyId) -> Option<&hmac::Tag> { self.cache.get(&key_id) } } pub fn periodic_rotate(rotor: Arc>) { let mut rotor = rotor; thread::spawn(move || loop { inner(&mut rotor); let restlen = read_sleep(&rotor); thread::sleep(Duration::from_secs(restlen)); }); } fn inner(rotor: &mut Arc>) { let _ = rotor.write().unwrap().rotate(); } fn read_sleep(rotor: &Arc>) -> u64 { rotor.read().unwrap().duration } // ------------------------------------------------------------------------ // Tests // ------------------------------------------------------------------------ #[cfg(test)] use ::memcache::MemcacheError; #[cfg(test)] use test::memcache; #[cfg(test)] use test::SystemTime; #[cfg(test)] mod test { use super::*; use ::memcache::MemcacheError; use lazy_static::lazy_static; use sloggers::null::NullLoggerBuilder; use sloggers::Build; use std::sync::Mutex; use std::time::Duration; // Mocking memcache. pub mod memcache { use super::*; use std::collections::HashMap; lazy_static! { pub static ref HASH_MAP: Mutex>> = Mutex::new(HashMap::new()); } pub struct Client; impl Client { pub fn connect(_url: &str) -> Result { Ok(Client) } pub fn get(&mut self, key: &str) -> Result>, MemcacheError> { Ok(HASH_MAP.lock().unwrap().get(&String::from(key)).cloned()) } } } // Mocking SystemTime. lazy_static! { pub static ref NOW: Mutex = Mutex::new(0); } pub struct SystemTime; impl SystemTime { pub fn now() -> std::time::SystemTime { let now = NOW.lock().unwrap(); let duration = Duration::new(*now, 0); UNIX_EPOCH.checked_add(duration).unwrap() } } #[test] fn test_rotation() { use self::memcache::HASH_MAP; let mut hash_map = HASH_MAP.lock().unwrap(); hash_map.insert("test/1".to_string(), vec![1; 32]); hash_map.insert("test/2".to_string(), vec![2; 32]); hash_map.insert("test/3".to_string(), vec![3; 32]); hash_map.insert("test/4".to_string(), vec![4; 32]); drop(hash_map); let mut rotator = KeyRotator { memcached_url: String::from("unused"), prefix: String::from("test"), duration: 1, number_of_forward_periods: 1, number_of_backward_periods: 1, master_key: CookieKey::from(&[0, 32][..]), latest_key_id: KeyId::from_be_bytes([1, 2, 3, 4]), cache: HashMap::new(), logger: NullLoggerBuilder.build().unwrap(), }; *NOW.lock().unwrap() = 2; // No error because the hash map has "test/1", "test/2", and "test/3". rotator.rotate().unwrap(); let old_latest = rotator.latest_key_id; *NOW.lock().unwrap() = 3; // No error because the hash map has "test/2", "test/3", and "test/4". rotator.rotate().unwrap(); let new_latest = rotator.latest_key_id; // The key id should change. assert_ne!(old_latest, new_latest); *NOW.lock().unwrap() = 1; // Return error because the hash map doesn't have "test/0". rotator.rotate().unwrap_err(); *NOW.lock().unwrap() = 4; // Return error because the hash map doesn't have "test/5". rotator.rotate().unwrap_err(); } } ================================================ FILE: src/main.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. extern crate lazy_static; extern crate log; extern crate prometheus; extern crate slog; extern crate slog_scope; extern crate slog_stdlog; extern crate sloggers; mod cfsock; mod cmd; mod cookie; mod error; mod key_rotator; mod metrics; mod ntp; mod nts_ke; mod sub_command; use sloggers::terminal::{Destination, TerminalLoggerBuilder}; use sloggers::types::Severity; use sloggers::Build; use std::process; /// Create a logger to be used throughout cfnts. fn create_logger(matches: &clap::ArgMatches<'_>) -> slog::Logger { let mut builder = TerminalLoggerBuilder::new(); // Default severity level is info. builder.level(Severity::Info); // Write all logs to stderr. builder.destination(Destination::Stderr); // If in debug mode, change severity level to debug. if matches.is_present("debug") { builder.level(Severity::Debug); } // According to `sloggers-0.3.2` source code, the function doesn't return an error at all. // There should be no problem unwrapping here. It has a return type `Result` because it's a // signature for `sloggers::Build` trait. builder .build() .expect("BUG: TerminalLoggerBuilder::build shouldn't return an error.") } /// The entry point of cfnts. fn main() { // According to the documentation of `get_matches`, if the parsing fails, an error will be // displayed to the user and the process will exit with an error code. let matches = cmd::create_clap_command().get_matches(); let logger = create_logger(&matches); // After calling this, slog_stdlog will forward all the `log` crate logging to // `slog_scope::logger()`. // // The returned error type is `SetLoggerError` which, according to the lib doc, will be // returned only when `set_logger` has been called already which should be our bug if it // has already been called. // slog_stdlog::init().expect("BUG: `set_logger` has already been called"); // _scope_guard can be used to reset the global logger. You can do it by just dropping it. let _scope_guard = slog_scope::set_global_logger(logger.clone()); if matches.subcommand.is_none() { eprintln!( "please specify a valid subcommand: only client, ke-server, and ntp-server \ are supported." ); process::exit(1); } if let Some(ke_server_matches) = matches.subcommand_matches("ke-server") { sub_command::ke_server::run(ke_server_matches); } if let Some(ntp_server_matches) = matches.subcommand_matches("ntp-server") { sub_command::ntp_server::run(ntp_server_matches); } if let Some(client_matches) = matches.subcommand_matches("client") { sub_command::client::run(client_matches); } } ================================================ FILE: src/metrics.rs ================================================ // Our goal is to shove data at prometheus in response to requests. use lazy_static::lazy_static; use prometheus::{self, register_int_gauge, Encoder, __register_gauge, labels, opts}; use std::io; use std::io::{BufRead, BufReader, Write}; use std::net; use std::thread; use slog::error; #[derive(Clone, Debug)] pub struct MetricsConfig { pub port: u16, pub addr: String, } const VERSION: &str = env!("CARGO_PKG_VERSION"); lazy_static! { static ref VERSION_INFO: prometheus::IntGauge = register_int_gauge!(opts!( "build_info", "Build and version information", labels! { "version" => VERSION, } )) .unwrap(); } fn wait_for_req_or_eof(dest: &net::TcpStream, logger: slog::Logger) -> Result<(), io::Error> { let mut reader = BufReader::new(dest); let mut req_line = String::new(); let mut done = false; while !done { req_line.clear(); let res = reader.read_line(&mut req_line); if let Err(e) = res { error!(logger, "failure to read request {:?}", e); return Err(e); } if let Ok(0) = res { // We got EOF ahead of request coming in // but will try to answer anyway done = true; } if req_line == "\r\n" { done = true; // terminates the request } } Ok(()) } fn scrape_result() -> String { let mut buffer = Vec::new(); let encoder = prometheus::TextEncoder::new(); let families = prometheus::gather(); encoder.encode(&families, &mut buffer).unwrap(); "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\n\r\n".to_owned() + &String::from_utf8(buffer).unwrap() } fn serve_metrics(mut dest: net::TcpStream, logger: slog::Logger) { if let Err(e) = wait_for_req_or_eof(&dest, logger.clone()) { error!( logger, "error in wait_for_req_or_eof: {:?}, unable to serve metrics", e ); if let Err(e) = dest.shutdown(net::Shutdown::Both) { error!(logger, "shutting down TcpStream failed with error: {:?}", e); } return; } if let Err(e) = dest.write(scrape_result().as_bytes()) { error!( logger, "write to TcpStream failed with error: {:?}, unable to serve metrics", e ); } if let Err(e) = dest.shutdown(net::Shutdown::Write) { error!(logger, "failure to shut down {:?}", e); } } /// Runs the metric server on the address and port set in config pub fn run_metrics(conf: MetricsConfig, logger: &slog::Logger) -> Result<(), std::io::Error> { VERSION_INFO.set(1); let accept = net::TcpListener::bind((conf.addr.as_str(), conf.port))?; for stream in accept.incoming() { match stream { Ok(conn) => { let log_metrics = logger.new(slog::o!("component"=>"serve_metrics")); thread::spawn(move || { serve_metrics(conn, log_metrics); }); } Err(err) => return Err(err), } } Err(io::Error::new(io::ErrorKind::Other, "unreachable")) } ================================================ FILE: src/ntp/client.rs ================================================ use crate::nts_ke::client::NtsKeResult; use miscreant::aead::Aead; use miscreant::aead::Aes128SivAead; use rand::Rng; use slog::debug; use std::error::Error; use std::fmt; use std::net::{ToSocketAddrs, UdpSocket}; use std::time::{Duration, SystemTime}; use super::protocol::parse_nts_packet; use super::protocol::serialize_nts_packet; use super::protocol::LeapState; use super::protocol::NtpExtension; use super::protocol::NtpExtensionType::*; use super::protocol::NtpPacketHeader; use super::protocol::NtsPacket; use super::protocol::PacketMode::Client; use super::protocol::TWO_POW_32; use super::protocol::UNIX_OFFSET; use self::NtpClientError::*; const BUFF_SIZE: usize = 2048; const TIMEOUT: Duration = Duration::from_secs(10); pub struct NtpResult { pub stratum: u8, pub time_diff: f64, } #[derive(Debug, Clone)] pub enum NtpClientError { NoIpv4AddrFound, NoIpv6AddrFound, InvalidUid, } impl std::error::Error for NtpClientError { fn description(&self) -> &str { match self { Self::NoIpv4AddrFound => { "Connection to server failed: IPv4 address could not be resolved" } Self::NoIpv6AddrFound => { "Connection to server failed: IPv6 address could not be resolved" } Self::InvalidUid => { "Connection to server failed: server response UID did not match client request UID" } } } fn cause(&self) -> Option<&dyn std::error::Error> { None } } impl std::fmt::Display for NtpClientError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Ntp Client Error ") } } /// Returns a float representing the system time as NTP fn system_to_ntpfloat(time: SystemTime) -> f64 { let unix_time = time.duration_since(SystemTime::UNIX_EPOCH).unwrap(); // Safe absent time machines let unix_offset = Duration::new(UNIX_OFFSET, 0); let epoch_time = unix_offset + unix_time; epoch_time.as_secs() as f64 + (epoch_time.subsec_nanos() as f64) / 1.0e9 } /// Returns a float representing the ntp timestamp fn timestamp_to_float(time: u64) -> f64 { let ts_secs = time >> 32; let ts_frac = time - (ts_secs << 32); (ts_secs as f64) + (ts_frac as f64) / TWO_POW_32 } /// Run the NTS client with the given data from key exchange pub fn run_nts_ntp_client( logger: &slog::Logger, state: NtsKeResult, ) -> Result> { let mut ip_addrs = (state.next_server.as_str(), state.next_port).to_socket_addrs()?; let addr; let socket; if let Some(use_ipv4) = state.use_ipv4 { if use_ipv4 { // mandated to use ipv4 addr = ip_addrs.find(|&x| x.is_ipv4()); if addr.is_none() { return Err(Box::new(NoIpv4AddrFound)); } socket = UdpSocket::bind("0.0.0.0:0"); } else { // mandated to use ipv6 addr = ip_addrs.find(|&x| x.is_ipv6()); if addr.is_none() { return Err(Box::new(NoIpv6AddrFound)); } socket = UdpSocket::bind("[::]:0"); } } else { // sniff whichever one is supported addr = ip_addrs.next(); // check if this address is ipv4 or ipv6 if addr.unwrap().is_ipv6() { socket = UdpSocket::bind("[::]:0"); } else { socket = UdpSocket::bind("0.0.0.0:0"); } } let socket = socket.unwrap(); socket.set_read_timeout(Some(TIMEOUT))?; socket.set_write_timeout(Some(TIMEOUT))?; let mut send_aead = Aes128SivAead::new(&state.keys.c2s); let mut recv_aead = Aes128SivAead::new(&state.keys.s2c); let header = NtpPacketHeader { leap_indicator: LeapState::NoLeap, version: 4, mode: Client, stratum: 0, poll: 0, precision: 0x20, root_delay: 0, root_dispersion: 0, reference_id: 0, reference_timestamp: 0xdeadbeef, origin_timestamp: 0, receive_timestamp: 0, transmit_timestamp: 0, }; let mut unique_id: Vec = vec![0; 32]; rand::thread_rng().fill(&mut unique_id[..]); let auth_exts = vec![ NtpExtension { ext_type: UniqueIdentifier, contents: unique_id.clone(), }, NtpExtension { ext_type: NTSCookie, contents: state.cookies[0].clone(), }, ]; let packet = NtsPacket { header, auth_exts, auth_enc_exts: vec![], }; socket.connect(addr.unwrap())?; let wire_packet = &serialize_nts_packet::(packet, &mut send_aead); let t1 = system_to_ntpfloat(SystemTime::now()); socket.send(wire_packet)?; debug!(logger, "transmitting packet"); let mut buff = [0; BUFF_SIZE]; let (size, _origin) = socket.recv_from(&mut buff)?; let t4 = system_to_ntpfloat(SystemTime::now()); debug!(logger, "received packet"); let received = parse_nts_packet::(&buff[0..size], &mut recv_aead); match received { Err(x) => Err(Box::new(x)), Ok(packet) => { // check if server response contains the same UniqueIdentifier as client request let resp_unique_id = packet.auth_exts[0].clone().contents; if resp_unique_id != unique_id { return Err(Box::new(InvalidUid)); } Ok(NtpResult { stratum: packet.header.stratum, time_diff: ((timestamp_to_float(packet.header.receive_timestamp) - t1) + (timestamp_to_float(packet.header.transmit_timestamp) - t4)) / 2.0, }) } } } ================================================ FILE: src/ntp/mod.rs ================================================ pub mod client; pub mod protocol; pub mod server; ================================================ FILE: src/ntp/protocol.rs ================================================ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use miscreant::aead::Aead; use rand::Rng; use std::io::{Cursor, Error, ErrorKind, Read, Write}; use std::panic; use self::LeapState::*; use self::NtpExtensionType::*; use self::PacketMode::*; /// These numbers are from RFC 5905 pub const VERSION: u8 = 4; pub const UNIX_OFFSET: u64 = 2_208_988_800; pub const PHI: f64 = 15e-6; /// TWO_POW_32 is a floating point power of two (2**32) pub const TWO_POW_32: f64 = 4294967296.0; const HEADER_SIZE: u64 = 48; const NONCE_LEN: usize = 16; const EXT_TYPE_UNIQUE_IDENTIFIER: u16 = 0x0104; const EXT_TYPE_NTS_COOKIE: u16 = 0x0204; const EXT_TYPE_NTS_COOKIE_PLACEHOLDER: u16 = 0x0304; const EXT_TYPE_NTS_AUTHENTICATOR: u16 = 0x0404; #[derive(Debug, Clone, Copy, PartialEq)] pub enum LeapState { NoLeap = 0, Positive = 1, Negative = 2, Unknown = 3, } #[derive(Debug, Clone, Copy, PartialEq)] pub enum PacketMode { SymmetricActive = 1, SymmetricPassive = 2, Client = 3, // We send Mode 3 packets and recieve Mode 4. Check the errata on 5905! Server = 4, Broadcast = 5, Invalid, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum NtpExtensionType { UniqueIdentifier, NTSCookie, NTSCookiePlaceholder, NTSAuthenticator, Unknown(u16), } fn wire_type(x: NtpExtensionType) -> u16 { match x { UniqueIdentifier => EXT_TYPE_UNIQUE_IDENTIFIER, NTSCookie => EXT_TYPE_NTS_COOKIE, NTSCookiePlaceholder => EXT_TYPE_NTS_COOKIE_PLACEHOLDER, NTSAuthenticator => EXT_TYPE_NTS_AUTHENTICATOR, NtpExtensionType::Unknown(y) => y, } } fn type_from_wire(ext: u16) -> NtpExtensionType { match ext { EXT_TYPE_UNIQUE_IDENTIFIER => UniqueIdentifier, EXT_TYPE_NTS_COOKIE => NTSCookie, EXT_TYPE_NTS_COOKIE_PLACEHOLDER => NTSCookiePlaceholder, EXT_TYPE_NTS_AUTHENTICATOR => NTSAuthenticator, y => NtpExtensionType::Unknown(y), } } /// Header of an NTP and NTS packet /// See RFC 5905 for meaning of these fields #[derive(Debug, Clone, Copy, PartialEq)] pub struct NtpPacketHeader { pub leap_indicator: LeapState, pub version: u8, pub mode: PacketMode, pub stratum: u8, pub poll: i8, pub precision: i8, pub root_delay: u32, pub root_dispersion: u32, pub reference_id: u32, pub reference_timestamp: u64, pub origin_timestamp: u64, pub receive_timestamp: u64, pub transmit_timestamp: u64, } /// The authenticating extension needs to be treated /// differently from all other extensions. We can't write it out /// until we know the data it authenticates, so the nts parsing /// and writing functions are a bit more complicated. /// It is up to the constructor to ensure that the contents of /// extensions are padded to length a multiple of 4 greater then or /// equal to 16, or 28 if they are the last extension. #[derive(Debug, Clone)] pub struct NtpExtension { pub ext_type: NtpExtensionType, pub contents: Vec, } /// An NTS packet has authenticated extensions and authenticated and encrypted /// extensions. All other extensions are ignored. #[derive(Debug, Clone)] pub struct NtsPacket { pub header: NtpPacketHeader, pub auth_exts: Vec, pub auth_enc_exts: Vec, } /// An NTP packet has a header and optional numbers of extensions. We ignore /// legacy mac entirely. #[derive(Debug, Clone)] pub struct NtpPacket { pub header: NtpPacketHeader, pub exts: Vec, } /// The first byte encodes these three fields in a bitpacked format. /// These 4 helper functions deal with that. /// See RFC 5905 Figure 8. fn parse_leap_indicator(first: u8) -> LeapState { match first >> 6 { 0 => NoLeap, 1 => Positive, 2 => Negative, _ => LeapState::Unknown, } } fn parse_version(first: u8) -> u8 { (first & 0x38) >> 3 } fn parse_mode(first: u8) -> PacketMode { let modnum = first & 0x07; match modnum { 1 => SymmetricActive, 2 => SymmetricPassive, 3 => Client, 4 => Server, 5 => Broadcast, _ => Invalid, } } /// The first byte packs 3 fields in. fn create_first(leap: LeapState, version: u8, mode: PacketMode) -> u8 { ((leap as u8) << 6) | ((version << 3) & 0x38) | ((mode as u8) & 0x07) } /// Extract an NTP packet header from packet and return an error if it cannot be done. pub fn parse_packet_header(packet: &[u8]) -> Result { let mut buff = Cursor::new(packet); if packet.len() < 48 { Err(Error::new(ErrorKind::InvalidInput, "Too short")) } else { let first = buff.read_u8()?; let stratum = buff.read_u8()?; let poll = buff.read_i8()?; let precision = buff.read_i8()?; let root_delay = buff.read_u32::()?; let root_dispersion = buff.read_u32::()?; let reference_id = buff.read_u32::()?; let reference_timestamp = buff.read_u64::()?; let origin_timestamp = buff.read_u64::()?; let receive_timestamp = buff.read_u64::()?; let transmit_timestamp = buff.read_u64::()?; Ok(NtpPacketHeader { leap_indicator: parse_leap_indicator(first), version: parse_version(first), mode: parse_mode(first), stratum, poll, precision, root_delay, root_dispersion, reference_id, reference_timestamp, origin_timestamp, receive_timestamp, transmit_timestamp, }) } } /// serialize_header returns a Vec containing the wire /// format of the header. pub fn serialize_header(head: NtpPacketHeader) -> Vec { let mut buff = Cursor::new(Vec::new()); let first = create_first(head.leap_indicator, head.version, head.mode); buff.write_u8(first) .expect("write to buffer failed, unable to serialize NtpPacketHeader"); buff.write_u8(head.stratum) .expect("write to buffer failed, unable to serialize NtpPacketHeader"); buff.write_i8(head.poll) .expect("write to buffer failed, unable to serialize NtpPacketHeader"); buff.write_i8(head.precision) .expect("write to buffer failed, unable to serialize NtpPacketHeader"); buff.write_u32::(head.root_delay) .expect("write to buffer failed, unable to serialize NtpPacketHeader"); buff.write_u32::(head.root_dispersion) .expect("write to buffer failed, unable to serialize NtpPacketHeader"); buff.write_u32::(head.reference_id) .expect("write to buffer failed, unable to serialize NtpPacketHeader"); buff.write_u64::(head.reference_timestamp) .expect("write to buffer failed, unable to serialize NtpPacketHeader"); buff.write_u64::(head.origin_timestamp) .expect("write to buffer failed, unable to serialize NtpPacketHeader"); buff.write_u64::(head.receive_timestamp) .expect("write to buffer failed, unable to serialize NtpPacketHeader"); buff.write_u64::(head.transmit_timestamp) .expect("write to buffer failed, unable to serialize NtpPacketHeader"); buff.into_inner() } /// parse_ntp_packet parses an NTP packet pub fn parse_ntp_packet(buff: &[u8]) -> Result { let header = parse_packet_header(buff)?; let exts = parse_extensions(&buff[48..])?; Ok(NtpPacket { header, exts }) } /// Properly parsing NTP extensions in accordance with RFC 7822 is not necessary /// since the legacy MAC will never be used by this code. fn parse_extensions(buff: &[u8]) -> Result, std::io::Error> { let mut reader = Cursor::new(buff); let mut retval = Vec::new(); while buff.len() - reader.position() as usize >= 4 { let ext_type = reader.read_u16::()?; let ext_len = reader.read_u16::()?; if ext_len % 4 != 0 { return Err(Error::new( ErrorKind::InvalidInput, "extension not on word boundary", )); } if ext_len < 4 { return Err(Error::new(ErrorKind::InvalidInput, "extension too short")); } let mut contents: Vec = vec![0; (ext_len - 4) as usize]; reader.read_exact(&mut contents)?; retval.push(NtpExtension { ext_type: type_from_wire(ext_type), contents, }) } Ok(retval) } /// serialize_ntp_packet returns the packet in wire format. pub fn serialize_ntp_packet(pack: NtpPacket) -> Vec { let mut buff = Cursor::new(Vec::new()); buff.write_all(&serialize_header(pack.header)) .expect("buffer write failed; can't serialize NtpPacket"); buff.write_all(&serialize_extensions(pack.exts)) .expect("buffer write failed; can't serialize NtpPacket"); buff.into_inner() } fn serialize_extensions(exts: Vec) -> Vec { let mut buff = Cursor::new(Vec::new()); for ext in exts { if ext.contents.len() % 4 != 0 { panic!("extension is the wrong length") } buff.write_u16::(wire_type(ext.ext_type)) .expect("buffer write failed; can't serialize Ntp Extensions"); buff.write_u16::((ext.contents.len() + 4) as u16) .expect("buffer write failed; can't serialize Ntp Extensions"); // The length includes the header buff.write_all(&ext.contents) .expect("buffer write failed; can't serialize Ntp Extensions"); } buff.into_inner() } /// has_extension returns true if the packet has an extension of the right kind pub fn has_extension(pack: &NtpPacket, kind: NtpExtensionType) -> bool { pack.exts .clone() .into_iter() .any(|ext| ext.ext_type == kind) } /// is_nts_packet returns true if this packet is plausibly an NTS packet. /// TODO: enforce rules tighter about uniqueness of some of these extensions. pub fn is_nts_packet(pack: &NtpPacket) -> bool { has_extension(pack, NTSCookie) && has_extension(pack, NTSAuthenticator) && has_extension(pack, UniqueIdentifier) } /// extract_extension retrieves the extension if it exists, and else none. pub fn extract_extension(pack: &NtpPacket, kind: NtpExtensionType) -> Option { pack.exts .clone() .into_iter() .find(|ext| ext.ext_type == kind) } /// parse_nts_packet parses an NTS packet. pub fn parse_nts_packet( buff: &[u8], decryptor: &mut T, ) -> Result { let header = parse_packet_header(buff)?; let mut reader = Cursor::new(buff); let mut auth_exts = Vec::new(); reader.set_position(HEADER_SIZE); while buff.len() - reader.position() as usize >= 4 { let ext_type = reader.read_u16::()?; let ext_len = (reader.read_u16::()? - 4) as usize; // RFC 7822 match type_from_wire(ext_type) { NTSAuthenticator => { let mut auth_ext_contents = vec![0; ext_len]; reader.read_exact(&mut auth_ext_contents)?; let oldpos = (reader.position() - 4 - (ext_len as u64)) as usize; let enc_ext_data = parse_decrypt_auth_ext::(&buff[0..oldpos], &auth_ext_contents, decryptor)?; let auth_enc_exts = parse_extensions(&enc_ext_data)?; return Ok(NtsPacket { header, auth_exts, auth_enc_exts, }); } _ => { let mut contents: Vec = vec![0; ext_len]; reader.read_exact(&mut contents)?; auth_exts.push(NtpExtension { ext_type: type_from_wire(ext_type), contents, }); } } } Err(Error::new( ErrorKind::InvalidInput, "never saw the authenticator", )) } fn parse_decrypt_auth_ext( auth_dat: &[u8], auth_ext_contents: &[u8], decryptor: &mut T, ) -> Result, std::io::Error> { let mut reader = Cursor::new(auth_ext_contents); if auth_ext_contents.len() - (reader.position() as usize) < 4 { return Err(Error::new(ErrorKind::InvalidInput, "insufficient length")); } let nonce_len = reader.read_u16::()? as usize; let cipher_len = reader.read_u16::()? as usize; let nonce_pad_len = nonce_len + ((4 - (nonce_len % 4)) % 4); let cipher_pad_len = cipher_len + ((4 - (cipher_len % 4)) % 4); if nonce_pad_len + cipher_pad_len + 4 > auth_ext_contents.len() { return Err(Error::new( ErrorKind::InvalidInput, "length of data exceeds wrapper", )); } let nonce = &auth_ext_contents[4..(4 + nonce_len)]; let ciphertext = &auth_ext_contents[(4 + nonce_pad_len)..(4 + nonce_pad_len + cipher_len)]; let res = decryptor.open(nonce, auth_dat, ciphertext); if res.is_err() { return Err(Error::new(ErrorKind::InvalidInput, "authentication failed")); } Ok(res.unwrap()) } /// serialize_nts_packet serializes the packet and does all the encryption pub fn serialize_nts_packet(packet: NtsPacket, encryptor: &mut T) -> Vec { let mut buff = Cursor::new(Vec::new()); buff.write_all(&serialize_header(packet.header)) .expect("Nts header could not be written, failed to serialize NtsPacket"); buff.write_all(&serialize_extensions(packet.auth_exts)) .expect("Nts extensions could not be written, failed to serialize NtsPacket"); let plaintext = serialize_extensions(packet.auth_enc_exts); let mut nonce = [0; NONCE_LEN]; rand::thread_rng().fill(&mut nonce); let ciphertext = encryptor.seal(&nonce, buff.get_ref(), &plaintext); let mut authent_buffer = Cursor::new(Vec::new()); authent_buffer .write_u16::(NONCE_LEN as u16) .expect("Nonce length could not be written, failed to serialize NtsPacket"); // length of the nonce authent_buffer .write_u16::(ciphertext.len() as u16) .expect("Ciphertext length could not be written, failed to serialize NtsPacket"); authent_buffer .write_all(&nonce) .expect("Nonce could not be written, failed to serialize NtsPacket"); // 16 bytes so no padding authent_buffer .write_all(&ciphertext) .expect("Ciphertext could not be written, failed to serialize NtsPacket"); let padlen = (4 - (ciphertext.len() % 4)) % 4; for _i in 0..padlen { // pad with zeros: probably cleaner way exists authent_buffer .write_u8(0) .expect("Padding could not be written, failed to serialize NtsPacket"); } let last_ext = NtpExtension { ext_type: NTSAuthenticator, contents: authent_buffer.into_inner(), }; let res = serialize_extensions(vec![last_ext]); buff.write_all(&res) .expect("Extensions could not be written, failed to serialize NtsPacket"); buff.into_inner() } #[cfg(test)] mod tests { use super::*; use miscreant::aead::Aes128SivAead; #[test] fn test_ntp_header_parse() { let leaps = vec![NoLeap, Positive, Negative, LeapState::Unknown]; let versions = vec![1, 2, 3, 4, 5, 6, 7]; let modes = vec![SymmetricActive, SymmetricPassive, Client, Server, Broadcast]; for leap in &leaps { for version in &versions { for mode in &modes { let start_header = NtpPacketHeader { leap_indicator: *leap, version: *version, mode: *mode, stratum: 0, poll: 0, precision: 0, root_delay: 0, root_dispersion: 0, reference_id: 0, reference_timestamp: 0, origin_timestamp: 0, receive_timestamp: 0, transmit_timestamp: 0, }; let ret_header = parse_packet_header(&serialize_header(start_header)).unwrap(); assert_eq!(ret_header, start_header) } } } } fn check_eq_ext(a: &NtpExtension, b: &NtpExtension) { assert_eq!(a.ext_type, b.ext_type); assert_eq!(a.contents.len(), b.contents.len()); for i in 0..a.contents.len() { assert_eq!(a.contents[i], b.contents[i]); } } fn check_ext_array_eq(exts1: Vec, exts2: Vec) { assert_eq!(exts1.len(), exts2.len()); for i in 0..exts1.len() { check_eq_ext(&exts1[i], &exts2[i]); } } fn check_nts_match(pkt1: NtsPacket, pkt2: NtsPacket) { assert_eq!(pkt1.header, pkt2.header); check_ext_array_eq(pkt1.auth_enc_exts, pkt2.auth_enc_exts); check_ext_array_eq(pkt1.auth_exts, pkt2.auth_exts); } fn roundtrip_test(input: NtsPacket, enc: &mut T) { let mut packet = serialize_nts_packet::(input.clone(), enc); let decrypt = parse_nts_packet(&packet, enc).unwrap(); check_nts_match(input, decrypt); packet[0] = 0xde; packet[1] = 0xad; packet[2] = 0xbe; packet[3] = 0xef; if parse_nts_packet(&packet, enc).is_ok() { panic!("success when we should have failed"); } } #[test] fn test_nts_parse() { let key = [0; 32]; let mut test_aead = Aes128SivAead::new(&key); let header = NtpPacketHeader { leap_indicator: NoLeap, version: 4, mode: Client, stratum: 1, poll: 0, precision: 0, root_delay: 0, root_dispersion: 0, reference_id: 0, reference_timestamp: 0, origin_timestamp: 0, receive_timestamp: 0, transmit_timestamp: 0, }; let packet = NtsPacket { header, auth_exts: vec![ NtpExtension { ext_type: UniqueIdentifier, contents: vec![0; 32], }, NtpExtension { ext_type: NTSCookie, contents: vec![0; 32], }, ], auth_enc_exts: vec![NtpExtension { ext_type: NTSCookiePlaceholder, contents: vec![0xfe; 32], }], }; roundtrip_test::(packet, &mut test_aead); } } ================================================ FILE: src/ntp/server/config.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! NTP server configuration. use sloggers::terminal::TerminalLoggerBuilder; use sloggers::Build; use std::convert::TryFrom; use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; use crate::cookie::CookieKey; use crate::error::WrapError; use crate::metrics::MetricsConfig; fn get_metrics_config(settings: &config::Config) -> Option { let mut metrics = None; if let Ok(addr) = settings.get_str("metrics_addr") { if let Ok(port) = settings.get_int("metrics_port") { metrics = Some(MetricsConfig { port: port as u16, addr, }); } } metrics } /// Configuration for running an NTP server. #[derive(Debug)] pub struct NtpServerConfig { /// List of addresses and ports to the server will be listening to. // Each of the elements can be either IPv4 or IPv6 address. It cannot be a UNIX socket address. addrs: Vec, pub cookie_key: CookieKey, /// The logger that will be used throughout the application, while the server is running. /// This property is mandatory because logging is very important for debugging. logger: slog::Logger, pub memcached_url: String, pub metrics_config: Option, pub upstream_addr: Option, } /// We decided to make NtpServerConfig mutable so that you can add more address after you parse /// the config file. impl NtpServerConfig { /// Create a NTP server config object with the given cookie key, memcached url, the metrics /// config, and the upstream address port. pub fn new( cookie_key: CookieKey, memcached_url: String, metrics_config: Option, upstream_addr: Option, ) -> NtpServerConfig { NtpServerConfig { addrs: Vec::new(), // Use terminal logger as a default logger. The users can override it using // `set_logger` later, if they want. // // According to `sloggers-0.3.2` source code, the function doesn't return an error at // all. There should be no problem unwrapping here. logger: TerminalLoggerBuilder::new() .build() .expect("BUG: TerminalLoggerBuilder::build shouldn't return an error."), // From parameters. cookie_key, memcached_url, metrics_config, upstream_addr, } } /// Add an address into the config. pub fn add_address(&mut self, addr: SocketAddr) { self.addrs.push(addr); } /// Return a list of addresses. pub fn addrs(&self) -> &[SocketAddr] { self.addrs.as_slice() } /// Set a new logger to the config. pub fn set_logger(&mut self, logger: slog::Logger) { self.logger = logger; } /// Return the logger of the config. pub fn logger(&self) -> &slog::Logger { &self.logger } /// Parse a config from a file. /// /// # Errors /// /// Currently we return `config::ConfigError` which is returned from functions in the /// `config` crate itself. /// /// For any error from any file specified in the configuration, `std::io::Error` which is /// wrapped inside `config::ConfigError::Foreign` will be returned. /// /// For any address parsing error, `std::io::Error` wrapped inside /// `config::ConfigError::Foreign` will also be returned. /// /// In addition, it also returns some custom `config::ConfigError::Message` errors, for the /// following cases: /// /// * The upstream port in the configuration file is a valid `i64` but not a valid `u16`. /// // Returning a `Message` object here is not a good practice. I will figure out a good practice // later. pub fn parse(filename: &str) -> Result { let mut settings = config::Config::new(); settings.merge(config::File::with_name(filename))?; let memcached_url = settings.get_str("memc_url")?; // Resolves metrics configuration. let metrics_config = get_metrics_config(&settings); // XXX: The code of parsing a next port here is quite ugly due to the `get_int` interface. // Please don't be surprised :) let upstream_port = match settings.get_int("upstream_port") { // If it's a not-found error, we can just leave it empty. Err(config::ConfigError::NotFound(_)) => None, // If it's other error, for example, unparseable error, it means that the user intended // to enter the port number but it just fails. Err(error) => return Err(error), Ok(val) => { let port = match u16::try_from(val) { Ok(val) => val, // The error will happen when the port number is not in a range of `u16`. Err(_) => { // Returning a custom message is not a good practice, but we can improve // it later when we don't have to depend on `config` crate. return Err(config::ConfigError::Message(String::from( "the upstream port is not a valid u64", ))); } }; Some(port) } }; let upstream_addr = match settings.get_str("upstream_addr") { // If it's a not-found error, we can just leave it empty. Err(config::ConfigError::NotFound(_)) => None, // If it's other error, for example, unparseable error, it means that the user intended // to enter the address but it just fails. Err(error) => return Err(error), Ok(addr) => Some(addr), }; let upstream_sock_addr = if let (Some(upstream_addr), Some(upstream_port)) = (upstream_addr, upstream_port) { Some(SocketAddr::from(( IpAddr::from_str(&upstream_addr).wrap_err()?, upstream_port, ))) } else { None }; // Note that all of the file reading stuffs should be at the end of the function so that // all the not-file-related stuffs can fail fast. let cookie_key_filename = settings.get_str("cookie_key_file")?; let cookie_key = CookieKey::parse(&cookie_key_filename).wrap_err()?; let mut config = NtpServerConfig::new( cookie_key, memcached_url, metrics_config, upstream_sock_addr, ); let addrs = settings.get_array("addr")?; for addr in addrs { // Parse SocketAddr from a string. let sock_addr = addr.to_string().parse().wrap_err()?; config.add_address(sock_addr); } Ok(config) } } ================================================ FILE: src/ntp/server/mod.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! NTP server implementation. mod config; mod ntp_server; pub use self::config::NtpServerConfig; pub use self::ntp_server::start_ntp_server; ================================================ FILE: src/ntp/server/ntp_server.rs ================================================ use super::config::NtpServerConfig; use crate::cfsock; use crate::cookie::{eat_cookie, get_keyid, make_cookie, NTSKeys, COOKIE_SIZE}; use crate::key_rotator::{periodic_rotate, KeyRotator}; use crate::metrics; use lazy_static::lazy_static; use prometheus::{opts, register_counter, register_int_counter, IntCounter}; use slog::{error, info}; use std::io::{Error, ErrorKind}; use std::net::{SocketAddr, ToSocketAddrs, UdpSocket}; use std::os::unix::io::AsRawFd; use std::sync::{Arc, RwLock}; use std::thread; use std::time; use std::time::{Duration, SystemTime}; use std::vec; use crossbeam::sync::WaitGroup; use libc::{in6_pktinfo, in_pktinfo}; /// Miscreant calls Aes128SivAead what IANA calls AEAD_AES_SIV_CMAC_256 use miscreant::aead::Aead; use miscreant::aead::Aes128SivAead; use nix::sys::socket::{ recvmsg, sendmsg, setsockopt, sockopt, CmsgSpace, ControlMessage, MsgFlags, }; use nix::sys::time::{TimeVal, TimeValLike}; use nix::sys::uio::IoVec; use crate::ntp::protocol; use crate::ntp::protocol::{ extract_extension, has_extension, is_nts_packet, parse_ntp_packet, parse_nts_packet, serialize_header, serialize_ntp_packet, serialize_nts_packet, LeapState, LeapState::*, NtpExtension, NtpExtensionType::NTSCookie, NtpExtensionType::UniqueIdentifier, NtpPacket, NtpPacketHeader, NtsPacket, PacketMode, PHI, UNIX_OFFSET, }; const BUF_SIZE: usize = 1280; // Anything larger might fragment. const TWO_POW_32: f64 = 4294967296.0; const TWO_POW_16: f64 = 65536.0; lazy_static! { static ref QUERY_COUNTER: IntCounter = register_int_counter!("ntp_queries_total", "Number of NTP queries").unwrap(); static ref NTS_COUNTER: IntCounter = register_int_counter!( "ntp_nts_queries_total", "Number of queries we thought were NTS" ) .unwrap(); static ref KOD_COUNTER: IntCounter = register_int_counter!("ntp_kod_total", "Number of Kiss of Death packets sent").unwrap(); static ref MALFORMED_COOKIE_COUNTER: IntCounter = register_int_counter!( "ntp_malformed_cookie_total", "Number of cookies with malformations" ) .unwrap(); static ref MANGLED_PACKET_COUNTER: IntCounter = register_int_counter!( "ntp_mangled_packet_total", "Number of packets without valid ntp headers" ) .unwrap(); static ref MISSING_KEY_COUNTER: IntCounter = register_int_counter!("ntp_missing_key_total", "Number of keys we could not find").unwrap(); static ref UNDECRYPTABLE_COOKIE_COUNTER: IntCounter = register_int_counter!( "ntp_undecryptable_cookie_total", "Number of cookies we could not decrypt" ) .unwrap(); static ref UPSTREAM_QUERY_COUNTER: IntCounter = register_int_counter!( "ntp_upstream_queries_total", "Number of upstream queries sent" ) .unwrap(); static ref UPSTREAM_FAILURE_COUNTER: IntCounter = register_int_counter!( "ntp_upstream_failures_total", "Number of failed upstream queries" ) .unwrap(); } #[derive(Clone, Copy, Debug)] struct ServerState { leap: LeapState, stratum: u8, version: u8, poll: i8, precision: i8, root_delay: u32, root_dispersion: u32, refid: u32, refstamp: u64, taken: SystemTime, } type TheCmsgSpace = CmsgSpace<(TimeVal, CmsgSpace<(in_pktinfo, CmsgSpace)>)>; /// run_server runs the ntp server on the given socket. /// The caller has to set up the socket options correctly fn run_server( socket: UdpSocket, keys: Arc>, servstate: Arc>, logger: slog::Logger, ipv4: bool, ) -> Result<(), std::io::Error> { let sockfd = socket.as_raw_fd(); setsockopt(sockfd, sockopt::ReceiveTimestamp, &true) .expect("setsockopt failed; can't run ntp server"); if ipv4 { setsockopt(sockfd, sockopt::Ipv4PacketInfo, &true) .expect("setsockopt failed; can't run ntp server"); } else { setsockopt(sockfd, sockopt::Ipv6RecvPacketInfo, &true) .expect("setsockopt failed; can't run ntp server"); } // The following is adapted from the example in the nix crate docs: // https://docs.rs/nix/0.13.0/nix/sys/socket/enum.ControlMessage.html#variant.ScmTimestamp // Most of these functions are documented in manpages, and nix is a thin wrapper around them. loop { // Receive and respond to packets let mut buf = [0; BUF_SIZE]; let flags = MsgFlags::empty(); let mut cmsgspace = TheCmsgSpace::new(); let iov = [IoVec::from_mut_slice(&mut buf)]; let r = recvmsg(sockfd, &iov, Some(&mut cmsgspace), flags); if let Err(_err) = r { error!(logger, "error receiving message: {:?}", _err); continue; } let r = r.unwrap(); // this is safe because of previous if if r.address.is_none() { // No return address => we can't do anything continue; } let src = r.address.unwrap(); // We should only have a single cmsg of known type. // The nix crate implements a typesafe interface to cmsg, // hence some of the matching here. let mut r_time = TimeVal::nanoseconds(0); let mut msgs: Vec = Vec::new(); for msg in r.cmsgs() { match msg { ControlMessage::ScmTimestamp(&r_timestamp) => r_time = r_timestamp, ControlMessage::Ipv4PacketInfo(_inf) => { if ipv4 { msgs.push(msg); } else { error!(logger, "v6 connection got v4 info"); continue; } } ControlMessage::Ipv6PacketInfo(_inf) => { if !ipv4 { msgs.push(msg); } else { error!(logger, "v4 connection got v6 info"); continue; } } _ => { error!(logger, "unexpected control message"); continue; } } } let r_system = SystemTime::UNIX_EPOCH + Duration::new(r_time.tv_sec() as u64, r_time.tv_usec() as u32 * 1000); let t_system = SystemTime::now(); // We now have the receive times and the current time as SystemTimes let resp = response( &buf[..r.bytes], r_system, t_system, keys.clone(), servstate.clone(), logger.clone(), ); match resp { Ok(data) => { let resp = sendmsg( sockfd, &[IoVec::from_slice(&data)], &msgs, flags, Some(&src), ); if let Err(err) = resp { error!(logger, "error sending response: {:}", err); } } Err(_) => { MANGLED_PACKET_COUNTER.inc(); // The packet is too mangled to do much with. error!(logger, "mangled packet"); } }; } } /// start_ntp_server runs the ntp server with the config specified in config_filename pub fn start_ntp_server(config: NtpServerConfig) -> Result<(), Box> { let logger = config.logger().clone(); info!(logger, "Initializing keys with memcached"); let key_rotator = KeyRotator::connect( String::from("/nts/nts-keys"), // prefix config.memcached_url.clone(), // memcached_url config.cookie_key.clone(), // master_key logger.clone(), // logger ) .expect("error connecting to the memcached server"); let keys = Arc::new(RwLock::new(key_rotator)); periodic_rotate(keys.clone()); let servstate_struct = ServerState { leap: Unknown, stratum: 16, version: protocol::VERSION, poll: 7, precision: -18, root_delay: 10, root_dispersion: 10, refid: 0, refstamp: 0, taken: SystemTime::now(), }; let servstate = Arc::new(RwLock::new(servstate_struct)); match config.upstream_addr { Some(upstream_addr) => { info!(logger, "connecting to upstream"); let servstate = servstate.clone(); let rot_logger = logger.new(slog::o!("task"=>"refereshing servstate")); let socket = UdpSocket::bind("127.0.0.1:0")?; // we only go to local socket.set_read_timeout(Some(time::Duration::from_secs(1)))?; thread::spawn(move || { refresh_servstate(servstate, rot_logger, socket, &upstream_addr); }); } None => { let mut state_guard = servstate.write().unwrap(); info!(logger, "setting stratum to 1"); state_guard.leap = NoLeap; state_guard.stratum = 1; } } if let Some(metrics_config) = config.metrics_config.clone() { info!(logger, "spawning metrics"); let log_metrics = logger.new(slog::o!("component"=>"metrics")); thread::spawn(move || { metrics::run_metrics(metrics_config, &log_metrics) .expect("metrics could not be run; starting ntp server failed"); }); } let wg = WaitGroup::new(); for addr in config.addrs() { let addr = addr.to_socket_addrs().unwrap().next().unwrap(); let socket = cfsock::udp_listen(&addr)?; let wg = wg.clone(); let logger = logger.new(slog::o!("listen_addr"=>addr)); let keys = keys.clone(); let servstate = servstate.clone(); info!(logger, "Listening on: {}", socket.local_addr()?); let mut use_ipv4 = true; if let SocketAddr::V6(_) = addr { use_ipv4 = false; } thread::spawn(move || { run_server(socket, keys, servstate, logger, use_ipv4).expect("server could not be run"); drop(wg); }); } wg.wait(); Ok(()) } /// Compute the current dispersion to within 1 ULP. fn fix_dispersion(disp: u32, now: SystemTime, taken: SystemTime) -> u32 { let disp_frac = (disp & 0x0000ffff) as f64; let disp_secs = ((disp & 0xffff0000) >> 16) as f64; let dispf = disp_secs + disp_frac / TWO_POW_16; let diff = now.duration_since(taken); match diff { Ok(secs) => { let curdispf = dispf + (secs.as_secs() as f64) * PHI; let curdisp_secs = curdispf.floor() as u32; let curdisp_frac = (curdispf * 65336.0).floor() as u32; (curdisp_secs << 16) + curdisp_frac } Err(_) => disp, } } fn ntp_timestamp(time: SystemTime) -> u64 { let unix_time = time.duration_since(SystemTime::UNIX_EPOCH).unwrap(); // Safe absent time machines let unix_offset = Duration::new(UNIX_OFFSET, 0); let epoch_time = unix_offset + unix_time; let ts_secs = epoch_time.as_secs(); let ts_nanos = epoch_time.subsec_nanos() as f64; let ts_frac = ((ts_nanos * TWO_POW_32) / 1.0e9).round() as u32; // RFC 5905 Figure 3 (ts_secs << 32) + ts_frac as u64 } fn create_header( query_packet: &NtpPacket, received: SystemTime, transmit: SystemTime, servstate: Arc>, ) -> NtpPacketHeader { let servstate = servstate.read().unwrap(); let receive_timestamp = ntp_timestamp(received); let transmit_timestamp = ntp_timestamp(transmit); NtpPacketHeader { leap_indicator: servstate.leap, version: servstate.version, mode: PacketMode::Server, poll: servstate.poll, precision: servstate.precision, stratum: servstate.stratum, root_delay: servstate.root_delay, root_dispersion: fix_dispersion(servstate.root_dispersion, transmit, servstate.taken), reference_id: servstate.refid, reference_timestamp: servstate.refstamp, origin_timestamp: query_packet.header.transmit_timestamp, receive_timestamp, transmit_timestamp, } } fn response( query: &[u8], r_time: SystemTime, t_time: SystemTime, cookie_keys: Arc>, servstate: Arc>, logger: slog::Logger, ) -> Result, std::io::Error> { let query_packet = parse_ntp_packet(query)?; // Should try to send a KOD if this happens let resp_header = create_header(&query_packet, r_time, t_time, servstate); QUERY_COUNTER.inc(); if query_packet.header.mode != PacketMode::Client { return Err(Error::new(ErrorKind::InvalidData, "not client mode")); } if is_nts_packet(&query_packet) { NTS_COUNTER.inc(); let cookie = extract_extension(&query_packet, NTSCookie).unwrap(); let keyid_maybe = get_keyid(&cookie.contents); match keyid_maybe { Some(keyid) => { let point = cookie_keys.read().unwrap(); let key_maybe = (*point).get(keyid); match key_maybe { Some(key) => { let nts_keys = eat_cookie(&cookie.contents, key.as_ref()); match nts_keys { Some(nts_dir_keys) => Ok(process_nts( resp_header, nts_dir_keys, cookie_keys.clone(), query, )), None => { UNDECRYPTABLE_COOKIE_COUNTER.inc(); error!(logger, "undecryptable cookie with keyid {:x?}", keyid); send_kiss_of_death(query_packet) } } } None => { MISSING_KEY_COUNTER.inc(); error!(logger, "cannot access key {:x?}", keyid); send_kiss_of_death(query_packet) } } } None => { MALFORMED_COOKIE_COUNTER.inc(); error!(logger, "malformed cookie"); send_kiss_of_death(query_packet) } } } else { Ok(serialize_header(resp_header)) } } fn process_nts( resp_header: NtpPacketHeader, keys: NTSKeys, cookie_keys: Arc>, query_raw: &[u8], ) -> Vec { let mut recv_aead = Aes128SivAead::new(&keys.c2s); let mut send_aead = Aes128SivAead::new(&keys.s2c); let query = parse_nts_packet::(query_raw, &mut recv_aead); match query { Ok(packet) => serialize_nts_packet( nts_response(packet, resp_header, keys, cookie_keys), &mut send_aead, ), Err(_) => serialize_ntp_packet(kiss_of_death(parse_ntp_packet(query_raw).unwrap())), } } fn nts_response( query: NtsPacket, header: NtpPacketHeader, keys: NTSKeys, cookie_keys: Arc>, ) -> NtsPacket { let mut resp_packet = NtsPacket { header, auth_exts: vec![], auth_enc_exts: vec![], }; for ext in query.auth_exts { match ext.ext_type { protocol::NtpExtensionType::UniqueIdentifier => resp_packet.auth_exts.push(ext), protocol::NtpExtensionType::NTSCookiePlaceholder => { if ext.contents.len() >= COOKIE_SIZE { // Avoid amplification let keymaker = cookie_keys.read().unwrap(); let (key_id, curr_key) = keymaker.latest_key_value(); let cookie = make_cookie(keys, curr_key.as_ref(), key_id); resp_packet.auth_enc_exts.push(NtpExtension { ext_type: NTSCookie, contents: cookie, }) } } _ => {} } } // This is a free cookie to replace the one consumed in the packet let keymaker = cookie_keys.read().unwrap(); let (key_id, curr_key) = keymaker.latest_key_value(); let cookie = make_cookie(keys, curr_key.as_ref(), key_id); resp_packet.auth_enc_exts.push(NtpExtension { ext_type: NTSCookie, contents: cookie, }); resp_packet.header.transmit_timestamp = ntp_timestamp(SystemTime::now()); // Update at last possible time resp_packet } fn send_kiss_of_death(query_packet: NtpPacket) -> Result, std::io::Error> { let resp = kiss_of_death(query_packet); Ok(serialize_ntp_packet(resp)) } /// The kiss of death tells the client it has done something wrong. /// draft-ietf-ntp-using-nts-for-ntp-18 and RFC 5905 specify the format. fn kiss_of_death(query_packet: NtpPacket) -> NtpPacket { KOD_COUNTER.inc(); let kod_header = NtpPacketHeader { leap_indicator: LeapState::Unknown, version: 4, mode: PacketMode::Server, poll: 0, precision: 0, stratum: 0, root_delay: 0, root_dispersion: 0, reference_id: 0x4e54534e, // NTSN reference_timestamp: 0, origin_timestamp: query_packet.header.transmit_timestamp, receive_timestamp: 0, transmit_timestamp: 0, }; let mut kod_packet = NtpPacket { header: kod_header, exts: vec![], }; if has_extension(&query_packet, UniqueIdentifier) { kod_packet .exts .push(extract_extension(&query_packet, UniqueIdentifier).unwrap()); } kod_packet } fn refresh_servstate( servstate: Arc>, logger: slog::Logger, sock: std::net::UdpSocket, addr: &SocketAddr, ) { loop { let query_packet = NtpPacket { header: NtpPacketHeader { leap_indicator: LeapState::Unknown, version: 4, mode: PacketMode::Client, poll: 0, precision: 0, stratum: 0, root_delay: 0, root_dispersion: 0, reference_id: 0x0, reference_timestamp: 0, origin_timestamp: 0, receive_timestamp: 0, transmit_timestamp: 0, }, exts: vec![], }; sock.connect(addr) .expect("socket connection to server failed, failed to refresh server state"); sock.send(&serialize_ntp_packet(query_packet)) .expect("sending ntp packet to server failed, failed to refresh server state"); UPSTREAM_QUERY_COUNTER.inc(); let mut buff = [0; 2048]; let res = sock.recv_from(&mut buff); match res { Ok((size, _sender)) => { let response = parse_ntp_packet(&buff[0..size]); match response { Ok(packet) => { let mut state = servstate.write().unwrap(); state.leap = packet.header.leap_indicator; state.version = 4; state.poll = packet.header.poll; state.precision = packet.header.precision; state.stratum = packet.header.stratum; state.root_delay = packet.header.root_delay; state.root_dispersion = packet.header.root_dispersion; state.refid = packet.header.reference_id; state.refstamp = packet.header.reference_timestamp; state.taken = SystemTime::now(); info!(logger, "set server state with stratum {:}", state.stratum); } Err(err) => { UPSTREAM_FAILURE_COUNTER.inc(); error!(logger, "failure to parse response: {}", err); } } } Err(err) => { UPSTREAM_FAILURE_COUNTER.inc(); error!(logger, "read error: {}", err); } } thread::sleep(time::Duration::from_secs(1)); } } ================================================ FILE: src/nts_ke/client.rs ================================================ use slog::{debug, info}; use std::error::Error; use std::io::{Read, Write}; use std::net::{Shutdown, TcpStream, ToSocketAddrs}; use std::sync::Arc; use std::time::Duration; use rustls; use webpki; use webpki_roots; use super::records; use crate::cookie::NTSKeys; use crate::nts_ke::records::{ deserialize, process_record, // Functions. serialize, // Records. AeadAlgorithmRecord, // Errors. DeserializeError, EndOfMessageRecord, // Enums. KnownAeadAlgorithm, KnownNextProtocol, NextProtocolRecord, NtsKeParseError, Party, // Structs. ReceivedNtsKeRecordState, // Constants. HEADER_SIZE, }; use crate::sub_command::client::ClientConfig; type Cookie = Vec; const DEFAULT_NTP_PORT: u16 = 123; const DEFAULT_KE_PORT: u16 = 4460; const DEFAULT_SCHEME: u16 = 0; const TIMEOUT: Duration = Duration::from_secs(15); #[derive(Clone, Debug)] pub struct NtsKeResult { pub cookies: Vec, pub next_protocols: Vec, pub aead_scheme: u16, pub next_server: String, pub next_port: u16, pub keys: NTSKeys, pub use_ipv4: Option, } /// run_nts_client executes the nts client with the config in config file pub fn run_nts_ke_client( logger: &slog::Logger, client_config: ClientConfig, ) -> Result> { let mut tls_config = rustls::ClientConfig::new(); let alpn_proto = String::from("ntske/1"); let alpn_bytes = alpn_proto.into_bytes(); tls_config.set_protocols(&[alpn_bytes]); match client_config.trusted_cert { Some(cert) => { info!(logger, "loading custom trust root"); tls_config.root_store.add(&cert)?; } None => { tls_config .root_store .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); } } let rc_config = Arc::new(tls_config); let hostname = webpki::DNSNameRef::try_from_ascii_str(client_config.host.as_str()) .expect("server hostname is invalid"); let mut client = rustls::ClientSession::new(&rc_config, hostname); debug!(logger, "Connecting"); let mut port = DEFAULT_KE_PORT; if let Some(p) = client_config.port { port = p.parse::()?; } let mut ip_addrs = (client_config.host.as_str(), port).to_socket_addrs()?; let addr; if let Some(use_ipv4) = client_config.use_ipv4 { if use_ipv4 { // mandated to use ipv4 addr = ip_addrs.find(|&x| x.is_ipv4()); if addr.is_none() { return Err(Box::new(NtsKeParseError::NoIpv4AddrFound)); } } else { // mandated to use ipv6 addr = ip_addrs.find(|&x| x.is_ipv6()); if addr.is_none() { return Err(Box::new(NtsKeParseError::NoIpv6AddrFound)); } } } else { // sniff whichever one is supported addr = ip_addrs.next(); } let mut stream = TcpStream::connect_timeout(&addr.unwrap(), TIMEOUT)?; stream.set_read_timeout(Some(TIMEOUT))?; stream.set_write_timeout(Some(TIMEOUT))?; let mut tls_stream = rustls::Stream::new(&mut client, &mut stream); let next_protocol_record = NextProtocolRecord::from(vec![KnownNextProtocol::Ntpv4]); let aead_record = AeadAlgorithmRecord::from(vec![KnownAeadAlgorithm::AeadAesSivCmac256]); let end_record = EndOfMessageRecord; let clientrec = &mut serialize(next_protocol_record); clientrec.append(&mut serialize(aead_record)); clientrec.append(&mut serialize(end_record)); tls_stream.write_all(clientrec)?; tls_stream.flush()?; debug!(logger, "Request transmitted"); let keys = records::gen_key(tls_stream.sess).unwrap(); let mut state = ReceivedNtsKeRecordState { finished: false, next_protocols: Vec::new(), aead_scheme: Vec::new(), cookies: Vec::new(), next_server: None, next_port: None, }; while !state.finished { let mut header: [u8; HEADER_SIZE] = [0; HEADER_SIZE]; // We should use `read_exact` here because we always need to read 4 bytes to get the // header. if let Err(error) = tls_stream.read_exact(&mut header[..]) { return Err(Box::new(error)); } // Retrieve a body length from the 3rd and 4th bytes of the header. let body_length = u16::from_be_bytes([header[2], header[3]]); let mut body = vec![0; body_length as usize]; // `read_exact` the length of the body. if let Err(error) = tls_stream.read_exact(body.as_mut_slice()) { return Err(Box::new(error)); } // Reconstruct the whole record byte array to let the `records` module deserialize it. let mut record_bytes = Vec::from(&header[..]); record_bytes.append(&mut body); // `deserialize` has an invariant that the slice needs to be long enough to make it a // valid record, which in this case our slice is exactly as long as specified in the // length field. match deserialize(Party::Client, record_bytes.as_slice()) { Ok(record) => { let status = process_record(record, &mut state); match status { Ok(_) => {} Err(err) => { return Err(err); } } } Err(DeserializeError::UnknownNotCriticalRecord) => { // If it's not critical, just ignore the error. debug!(logger, "unknown record type"); } Err(DeserializeError::UnknownCriticalRecord) => { // TODO: This should propertly handled by sending an Error record. debug!(logger, "error: unknown critical record"); return Err(Box::new(std::io::Error::new( std::io::ErrorKind::Other, "unknown critical record", ))); } Err(DeserializeError::Parsing(error)) => { // TODO: This shouldn't be wrapped as a trait object. debug!(logger, "error: {}", error); return Err(Box::new(std::io::Error::new( std::io::ErrorKind::Other, error, ))); } } } debug!(logger, "saw the end of the response"); stream.shutdown(Shutdown::Write)?; let aead_scheme = if state.aead_scheme.is_empty() { DEFAULT_SCHEME } else { state.aead_scheme[0] }; Ok(NtsKeResult { aead_scheme, cookies: state.cookies, next_protocols: state.next_protocols, next_server: state.next_server.unwrap_or(client_config.host.clone()), next_port: state.next_port.unwrap_or(DEFAULT_NTP_PORT), keys, use_ipv4: client_config.use_ipv4, }) } ================================================ FILE: src/nts_ke/mod.rs ================================================ pub mod client; pub mod records; pub mod server; ================================================ FILE: src/nts_ke/records/aead_algorithm.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! AEAD Algorithm Negotiation record representation. use std::convert::TryFrom; use super::KeRecordTrait; use super::Party; #[derive(Clone, Copy)] pub enum KnownAeadAlgorithm { AeadAesSivCmac256, } impl KnownAeadAlgorithm { pub fn as_algorithm_id(&self) -> u16 { match self { KnownAeadAlgorithm::AeadAesSivCmac256 => 15, } } } pub struct AeadAlgorithmRecord(Vec); impl AeadAlgorithmRecord { pub fn algorithms(&self) -> &[KnownAeadAlgorithm] { self.0.as_slice() } } impl From> for AeadAlgorithmRecord { fn from(algorithms: Vec) -> AeadAlgorithmRecord { AeadAlgorithmRecord(algorithms) } } impl KeRecordTrait for AeadAlgorithmRecord { fn critical(&self) -> bool { // According to the spec, this critical bit is optional, but it's good to assign it as // critical. true } fn record_type() -> u16 { 4 } fn len(&self) -> u16 { // Because each protocol takes 2 bytes, we need to multiply it by 2. u16::try_from(self.0.len()) .ok() .and_then(|length| length.checked_mul(2)) .expect("the number of AEAD algorithms are too large") } fn into_bytes(self) -> Vec { let mut bytes = Vec::new(); for algorithm in self.0.iter() { // The spec said that the protocol id must be in network byte order, so we have to // convert it to the big endian order here. let algorithm_bytes = &algorithm.as_algorithm_id().to_be_bytes()[..]; bytes.append(&mut Vec::from(algorithm_bytes)) } bytes } fn from_bytes(_: Party, bytes: &[u8]) -> Result { // The body length must be even because each algorithm code take 2 bytes, so it's not // reasonable for the length to be odd. if bytes.len() % 2 != 0 { return Err(String::from( "the body length of AEAD Algorithm Negotiation must be even.", )); } let mut algorithms = Vec::new(); for word in bytes.chunks_exact(2) { let algorithm_code = u16::from_be_bytes([word[0], word[1]]); let algorithm = KnownAeadAlgorithm::AeadAesSivCmac256; if algorithm.as_algorithm_id() == algorithm_code { algorithms.push(algorithm); } else { return Err(String::from("unknown AEAD algorithm id")); } } Ok(AeadAlgorithmRecord(algorithms)) } } ================================================ FILE: src/nts_ke/records/end_of_message.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! End Of Message record representation. use super::KeRecordTrait; use super::Party; pub struct EndOfMessageRecord; impl KeRecordTrait for EndOfMessageRecord { fn critical(&self) -> bool { true } fn record_type() -> u16 { 0 } fn len(&self) -> u16 { 0 } fn into_bytes(self) -> Vec { Vec::new() } fn from_bytes(_: Party, bytes: &[u8]) -> Result { if !bytes.is_empty() { Err(String::from( "the body length of End Of Message must be zero.", )) } else { Ok(EndOfMessageRecord) } } } ================================================ FILE: src/nts_ke/records/error.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! Error record representation. use super::KeRecordTrait; use super::Party; enum ErrorKind { UnrecognizedCriticalRecord, BadRequest, } impl ErrorKind { fn as_code(&self) -> u16 { match self { ErrorKind::UnrecognizedCriticalRecord => 0, ErrorKind::BadRequest => 1, } } } pub struct ErrorRecord(ErrorKind); impl KeRecordTrait for ErrorRecord { fn critical(&self) -> bool { true } fn record_type() -> u16 { 2 } fn len(&self) -> u16 { 2 } fn into_bytes(self) -> Vec { let error_code = &self.0.as_code().to_be_bytes()[..]; Vec::from(error_code) } fn from_bytes(_: Party, bytes: &[u8]) -> Result { if bytes.len() != 2 { return Err(String::from("the body length of Error must be two.")); } let error_code = u16::from_be_bytes([bytes[0], bytes[1]]); let kind = ErrorKind::UnrecognizedCriticalRecord; if kind.as_code() == error_code { return Ok(ErrorRecord(kind)); } let kind = ErrorKind::BadRequest; if kind.as_code() == error_code { return Ok(ErrorRecord(kind)); } Err(String::from("unknown error code")) } } ================================================ FILE: src/nts_ke/records/mod.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! NTS-KE record representation. mod aead_algorithm; mod end_of_message; mod error; mod new_cookie; mod next_protocol; mod port; mod server; mod warning; // We pub use everything in the submodules. You can limit the scope of usage by putting it the // submodule itself. pub use self::aead_algorithm::*; pub use self::end_of_message::*; pub use self::error::*; pub use self::new_cookie::*; pub use self::next_protocol::*; pub use self::port::*; pub use self::server::*; pub use self::warning::*; use rustls::TLSError; use std::fmt; use crate::cookie::NTSKeys; pub const HEADER_SIZE: usize = 4; pub enum KeRecord { EndOfMessage(EndOfMessageRecord), NextProtocol(NextProtocolRecord), Error(ErrorRecord), Warning(WarningRecord), AeadAlgorithm(AeadAlgorithmRecord), NewCookie(NewCookieRecord), Server(ServerRecord), Port(PortRecord), } #[derive(Clone, Copy)] pub enum Party { Client, Server, } pub trait KeRecordTrait: Sized { fn critical(&self) -> bool; fn record_type() -> u16; fn len(&self) -> u16; // This function has to consume the object to avoid additional memory consumption. fn into_bytes(self) -> Vec; fn from_bytes(sender: Party, bytes: &[u8]) -> Result; } // ------------------------------------------------------------------------ // Serialization // ------------------------------------------------------------------------ /// Serialize the record into the network-ready format. pub fn serialize(record: T) -> Vec { let mut result = Vec::new(); // The first 16 bits will comprise a critical bit and the record type. let first_word: u16 = (u16::from(record.critical()) << 15) + T::record_type(); result.append(&mut Vec::from(&first_word.to_be_bytes()[..])); // The second 16 bits will be the length of the record body. result.append(&mut Vec::from(&record.len().to_be_bytes()[..])); // The rest is the content of the record. result.append(&mut record.into_bytes()); result } // ------------------------------------------------------------------------ // Deserialization // ------------------------------------------------------------------------ #[derive(Clone, Debug)] pub enum DeserializeError { Parsing(String), UnknownCriticalRecord, UnknownNotCriticalRecord, } /// Deserialize the network bytes into the record. /// /// # Panics /// /// If slice is shorter than the length specified in the length field. /// pub fn deserialize(sender: Party, bytes: &[u8]) -> Result { // The first bit of the first byte is the critical bit. let critical = bytes[0] >> 7 == 1; // The following 15 bits are the record type number. let record_type = u16::from_be_bytes([bytes[0] & 0x7, bytes[1]]); // The third and fourth bytes are the body length. let length = u16::from_be_bytes([bytes[2], bytes[3]]); // The body. let body = &bytes[4..4 + usize::from(length)]; macro_rules! deserialize_body { ( $( ($variant:ident, $record:ident) ),* ) => { if false { // Loop returns ! type. loop { } } $( else if record_type == $record::record_type() { match $record::from_bytes(sender, body) { Ok(record) => KeRecord::$variant(record), Err(error) => return Err(DeserializeError::Parsing(error)), } } )* else { if critical { return Err(DeserializeError::UnknownCriticalRecord); } else { return Err(DeserializeError::UnknownNotCriticalRecord); } } }; } let record = deserialize_body!( (EndOfMessage, EndOfMessageRecord), (NextProtocol, NextProtocolRecord), (Error, ErrorRecord), (Warning, WarningRecord), (AeadAlgorithm, AeadAlgorithmRecord), (NewCookie, NewCookieRecord), (Server, ServerRecord), (Port, PortRecord) ); Ok(record) } /// gen_key computes the client and server keys using exporters. /// https://tools.ietf.org/html/draft-ietf-ntp-using-nts-for-ntp-28#section-4.3 pub fn gen_key(session: &T) -> Result { let mut keys: NTSKeys = NTSKeys { c2s: [0; 32], s2c: [0; 32], }; let c2s_con = [0, 0, 0, 15, 0]; let s2c_con = [0, 0, 0, 15, 1]; let context_c2s = Some(&c2s_con[..]); let context_s2c = Some(&s2c_con[..]); let label = "EXPORTER-network-time-security".as_bytes(); session.export_keying_material(&mut keys.c2s, label, context_c2s)?; session.export_keying_material(&mut keys.s2c, label, context_s2c)?; Ok(keys) } // ------------------------------------------------------------------------ // Record Process // ------------------------------------------------------------------------ type Cookie = Vec; #[derive(Clone, Debug)] pub struct ReceivedNtsKeRecordState { pub finished: bool, pub next_protocols: Vec, pub aead_scheme: Vec, pub cookies: Vec, pub next_server: Option, pub next_port: Option, } #[derive(Debug, Clone)] pub enum NtsKeParseError { RecordAfterEnd, ErrorRecord, NoIpv4AddrFound, NoIpv6AddrFound, } impl std::error::Error for NtsKeParseError { fn description(&self) -> &str { match self { Self::RecordAfterEnd => "Received record after connection finished", Self::ErrorRecord => "Received NTS error record", Self::NoIpv4AddrFound => { "Connection to server failed: IPv4 address could not be resolved" } Self::NoIpv6AddrFound => { "Connection to server failed: IPv6 address could not be resolved" } } } fn cause(&self) -> Option<&dyn std::error::Error> { None } } impl fmt::Display for NtsKeParseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "NTS-KE Record Parse Error") } } /// Read https://datatracker.ietf.org/doc/html/rfc8915#section-4 pub fn process_record( record: KeRecord, state: &mut ReceivedNtsKeRecordState, ) -> Result<(), Box> { if state.finished { return Err(Box::new(NtsKeParseError::RecordAfterEnd)); } match record { KeRecord::EndOfMessage(_) => state.finished = true, KeRecord::NextProtocol(record) => { state.next_protocols = record .protocols() .iter() .map(|protocol| protocol.as_protocol_id()) .collect(); } KeRecord::Error(_) => return Err(Box::new(NtsKeParseError::ErrorRecord)), KeRecord::Warning(_) => return Ok(()), KeRecord::AeadAlgorithm(record) => { state.aead_scheme = record .algorithms() .iter() .map(|algorithm| algorithm.as_algorithm_id()) .collect(); } KeRecord::NewCookie(record) => state.cookies.push(record.into_bytes()), KeRecord::Server(record) => state.next_server = Some(record.into_string()), KeRecord::Port(record) => state.next_port = Some(record.port()), } Ok(()) } ================================================ FILE: src/nts_ke/records/new_cookie.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! New Cookie record representation. use std::convert::TryFrom; use super::KeRecordTrait; use super::Party; pub struct NewCookieRecord(Vec); impl From> for NewCookieRecord { fn from(bytes: Vec) -> NewCookieRecord { NewCookieRecord(bytes) } } impl KeRecordTrait for NewCookieRecord { fn critical(&self) -> bool { false } fn record_type() -> u16 { 5 } fn len(&self) -> u16 { u16::try_from(self.0.len()).expect("the cookie is too large to fit in the record") } fn into_bytes(self) -> Vec { self.0 } fn from_bytes(_: Party, bytes: &[u8]) -> Result { // There is error for New Cookie record, because any byte slice is considered a valid // cookie. Ok(NewCookieRecord::from(Vec::from(bytes))) } } ================================================ FILE: src/nts_ke/records/next_protocol.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! NTS Next Protocol Negotiation record representation. use std::convert::TryFrom; use super::KeRecordTrait; use super::Party; #[derive(Clone, Copy)] pub enum KnownNextProtocol { Ntpv4, } impl KnownNextProtocol { pub fn as_protocol_id(&self) -> u16 { match self { KnownNextProtocol::Ntpv4 => 0, } } } pub struct NextProtocolRecord(Vec); impl NextProtocolRecord { pub fn protocols(&self) -> &[KnownNextProtocol] { self.0.as_slice() } } impl From> for NextProtocolRecord { fn from(protocols: Vec) -> NextProtocolRecord { NextProtocolRecord(protocols) } } impl KeRecordTrait for NextProtocolRecord { fn critical(&self) -> bool { true } fn record_type() -> u16 { 1 } fn len(&self) -> u16 { // Because each protocol takes 2 bytes, we need to multiply it by 2. u16::try_from(self.0.len()) .ok() .and_then(|length| length.checked_mul(2)) .expect("the number of next protocols are too large") } fn into_bytes(self) -> Vec { let mut bytes = Vec::new(); for protocol in self.0.iter() { // The spec said that the protocol id must be in network byte order, so we have to // convert it to the big endian order here. let protocol_bytes = &protocol.as_protocol_id().to_be_bytes()[..]; bytes.append(&mut Vec::from(protocol_bytes)) } bytes } fn from_bytes(_: Party, bytes: &[u8]) -> Result { // The body length must be even because each protocol code take 2 bytes, so it's not // reasonable for the length to be odd. if bytes.len() % 2 != 0 { return Err(String::from( "the body length of Next Protocol Negotiation must be even.", )); } let mut protocols = Vec::new(); for word in bytes.chunks_exact(2) { let protocol_code = u16::from_be_bytes([word[0], word[1]]); let protocol = KnownNextProtocol::Ntpv4; if protocol.as_protocol_id() == protocol_code { protocols.push(protocol); } else { return Err(String::from("unknown Next Protocol id")); } } Ok(NextProtocolRecord(protocols)) } } ================================================ FILE: src/nts_ke/records/port.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! Port negotiation record representation. /// This Port negotiation will not be sent from the server because currently, we are not /// interested in running an NTP server on different port. use super::KeRecordTrait; use super::Party; pub struct PortRecord { sender: Party, port: u16, } impl PortRecord { pub fn new(sender: Party, port: u16) -> PortRecord { PortRecord { sender, port } } pub fn port(&self) -> u16 { self.port } } impl KeRecordTrait for PortRecord { fn critical(&self) -> bool { match self.sender { Party::Client => false, Party::Server => true, } } fn record_type() -> u16 { 7 } fn len(&self) -> u16 { 2 } fn into_bytes(self) -> Vec { Vec::from(&self.port.to_be_bytes()[..]) } fn from_bytes(sender: Party, bytes: &[u8]) -> Result { if bytes.len() != 2 { Err(String::from("the body length of Port must be two.")) } else { let port = u16::from_be_bytes([bytes[0], bytes[1]]); Ok(PortRecord { sender, port }) } } } ================================================ FILE: src/nts_ke/records/server.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! Server negotiation record representation. /// This Server negotiation will not be sent from the server because currently, we are not /// interested in running an NTP server on different IP address. use std::convert::TryFrom; use std::net::Ipv4Addr; use std::net::Ipv6Addr; use std::str::FromStr; use super::KeRecordTrait; use super::Party; enum Address { Hostname(String), Ipv4Addr(Ipv4Addr), Ipv6Addr(Ipv6Addr), } pub struct ServerRecord { sender: Party, address: Address, } impl ServerRecord { pub fn into_string(self) -> String { match self.address { Address::Hostname(name) => name, Address::Ipv4Addr(addr) => addr.to_string(), Address::Ipv6Addr(addr) => addr.to_string(), } } } impl KeRecordTrait for ServerRecord { fn critical(&self) -> bool { match self.sender { Party::Client => false, Party::Server => true, } } fn record_type() -> u16 { 6 } fn len(&self) -> u16 { match &self.address { // We cannot just use `name.len()` because we want to count the bytes not just the // runes. Address::Hostname(name) => u16::try_from(name.as_bytes().len()) .expect("the hostname is too long to fix in the record"), // Both IPv4 and IPv6 address cannot be too long to fix in the record. It's okay to // just cast them here. Address::Ipv4Addr(addr) => addr.to_string().len() as u16, Address::Ipv6Addr(addr) => addr.to_string().len() as u16, } } fn into_bytes(self) -> Vec { Vec::from(self.into_string()) } fn from_bytes(sender: Party, bytes: &[u8]) -> Result { let body = match String::from_utf8(Vec::from(bytes)) { Ok(body) => body, Err(_) => return Err(String::from("the body is an invalid ascii string")), }; if !body.is_ascii() { return Err(String::from("the body is an invalid ascii string")); } let address = if let Ok(address) = Ipv4Addr::from_str(&body) { Address::Ipv4Addr(address) } else if let Ok(address) = Ipv6Addr::from_str(&body) { Address::Ipv6Addr(address) } else { // If the body is a valid ascii string, but not a valid IPv4 or IPv6, it must be a // hostname. Address::Hostname(body) }; Ok(ServerRecord { sender, address }) } } ================================================ FILE: src/nts_ke/records/warning.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! Warning record representation. use super::KeRecordTrait; use super::Party; enum WarningKind { // There is currently no warning specified in the spec, but we need to put something here to // make the code compiles. Please remove this Dummy when there is a warning specified in the // spec. Dummy, } impl WarningKind { fn as_code(&self) -> u16 { match self { // Put the max value for Dummy just to avoid colliding with the future warning code. WarningKind::Dummy => u16::max_value(), } } } pub struct WarningRecord(WarningKind); impl KeRecordTrait for WarningRecord { fn critical(&self) -> bool { true } fn record_type() -> u16 { 3 } fn len(&self) -> u16 { 2 } fn into_bytes(self) -> Vec { let error_code = &self.0.as_code().to_be_bytes()[..]; Vec::from(error_code) } fn from_bytes(_: Party, bytes: &[u8]) -> Result { if bytes.len() != 2 { return Err(String::from("the body length of Warning must be two.")); } let warning_code = u16::from_be_bytes([bytes[0], bytes[1]]); let kind = WarningKind::Dummy; if kind.as_code() == warning_code { return Ok(WarningRecord(kind)); } Err(String::from("unknown warning code")) } } ================================================ FILE: src/nts_ke/server/config.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! NTS-KE server configuration. use rustls::internal::pemfile; use rustls::{Certificate, PrivateKey}; use sloggers::terminal::TerminalLoggerBuilder; use sloggers::Build; use std::convert::TryFrom; use std::fs::File; use std::net::SocketAddr; use crate::cookie::CookieKey; use crate::error::WrapError; use crate::metrics::MetricsConfig; fn get_metrics_config(settings: &config::Config) -> Option { let mut metrics = None; if let Ok(addr) = settings.get_str("metrics_addr") { if let Ok(port) = settings.get_int("metrics_port") { metrics = Some(MetricsConfig { port: port as u16, addr, }); } } metrics } /// Configuration for running an NTS-KE server. #[derive(Debug)] pub struct KeServerConfig { /// List of addresses and ports to the server will be listening to. // Each of the elements can be either IPv4 or IPv6 address. It cannot be a UNIX socket address. addrs: Vec, /// The initial cookie key for the NTS-KE server. cookie_key: CookieKey, // If you don't to have a timeout, just set it to a very high value. timeout: u64, /// The logger that will be used throughout the application, while the server is running. /// This property is mandatory because logging is very important for debugging. logger: slog::Logger, /// The url of the memcached server. The memcached server is used to sync data between the /// NTS-KE server and the NTP server. memcached_url: String, pub metrics_config: Option, pub next_port: u16, pub tls_certs: Vec, pub tls_secret_keys: Vec, } /// We decided to make KeServerConfig mutable so that you can add more cert, private key, or /// address after you parse the config file. impl KeServerConfig { /// Create a NTS-KE server config object with the given next port, memcached url, connection /// timeout, and the metrics config. pub fn new( timeout: u64, cookie_key: CookieKey, memcached_url: String, metrics_config: Option, next_port: u16, ) -> KeServerConfig { KeServerConfig { addrs: Vec::new(), // Use terminal logger as a default logger. The users can override it using // `set_logger` later, if they want. // // According to `sloggers-0.3.2` source code, the function doesn't return an error at // all. There should be no problem unwrapping here. logger: TerminalLoggerBuilder::new() .build() .expect("BUG: TerminalLoggerBuilder::build shouldn't return an error."), tls_certs: Vec::new(), tls_secret_keys: Vec::new(), // From parameters. cookie_key, timeout, memcached_url, metrics_config, next_port, } } /// Add a TLS certificate into the config. // Because the order of `tls_certs` has to correspond to the order of `tls_secret_keys`, this // method has to be private for now. fn add_tls_cert(&mut self, cert: Certificate) { self.tls_certs.push(cert); } /// Add a TLS private key into the config. // Because the order of `tls_certs` has to correspond to the order of `tls_secret_keys`, this // method has to be private for now. fn add_tls_secret_key(&mut self, secret_key: PrivateKey) { self.tls_secret_keys.push(secret_key); } /// Add an address into the config. pub fn add_address(&mut self, addr: SocketAddr) { self.addrs.push(addr); } /// Return a list of addresses. pub fn addrs(&self) -> &[SocketAddr] { self.addrs.as_slice() } /// Return the cookie key of the config. pub fn cookie_key(&self) -> &CookieKey { &self.cookie_key } /// Set a new logger to the config. pub fn set_logger(&mut self, logger: slog::Logger) { self.logger = logger; } /// Return the logger of the config. pub fn logger(&self) -> &slog::Logger { &self.logger } /// Return the memcached url of the config. pub fn memcached_url(&self) -> &str { &self.memcached_url } /// Return the connection timeout of the config. pub fn timeout(&self) -> u64 { self.timeout } /// Import TLS certificates from a file. /// /// # Errors /// /// There will be an error if we cannot open the file or the content is not parsable to get /// certificates. /// // Because the order of `tls_certs` has to correspond to the order of `tls_secret_keys`, this // method has to be private for now. fn import_tls_certs(&mut self, filename: &str) -> Result<(), std::io::Error> { // Open a file. If there is any error, return it immediately. let file = File::open(filename)?; match pemfile::certs(&mut std::io::BufReader::new(file)) { Ok(certs) => { // Add all parsed certificates. for cert in certs { self.add_tls_cert(cert); } // Return success. Ok(()) } // We don't use Err(_) here because if the error type of `rustls` changes in the // future, we will get noticed. // // The `std::io` module has an error kind of `InvalidData` which is perfectly // suitable for our kind of error. Err(()) => Err(std::io::Error::new( std::io::ErrorKind::InvalidData, format!("cannot parse TLS certificates from {}", filename), )), } } /// Import TLS private keys from a file. /// /// # Errors /// /// There will be an error if we cannot open the file or the content is not parsable to get /// private keys. /// // Because the order of `tls_certs` has to correspond to the order of `tls_secret_keys`, this // method has to be private for now. fn import_tls_secret_keys(&mut self, filename: &str) -> Result<(), std::io::Error> { // Open a file. If there is any error, return it immediately. let file = File::open(filename)?; match pemfile::pkcs8_private_keys(&mut std::io::BufReader::new(file)) { Ok(secret_keys) => { // Add all parsed secret keys. for secret_key in secret_keys { self.add_tls_secret_key(secret_key); } // Return success. Ok(()) } // We don't use Err(_) here because if the error type of `rustls` changes in the // future, we will get noticed. // // The `std::io` module has an error kind of `InvalidData` which is perfectly // suitable for our kind of error. Err(()) => Err(std::io::Error::new( std::io::ErrorKind::InvalidData, format!("cannot parse TLS private keys from {}", filename), )), } } /// Parse a config from a file. /// /// # Errors /// /// Currently we return `config::ConfigError` which is returned from functions in the /// `config` crate itself. /// /// For any error from any file specified in the configuration, `std::io::Error` which is /// wrapped inside `config::ConfigError::Foreign` will be returned. /// /// For any address parsing error, `std::io::Error` wrapped inside /// `config::ConfigError::Foreign` will also be returned. /// /// In addition, it also returns some custom `config::ConfigError::Message` errors, for the /// following cases: /// /// * The next port in the configuration file is a valid `i64` but not a valid `u16`. /// * The connection timeout in the configuration file is a valid `i64` but not a valid `u64`. /// // Returning a `Message` object here is not a good practice. I will figure out a good practice // later. pub fn parse(filename: &str) -> Result { let mut settings = config::Config::new(); settings.merge(config::File::with_name(filename))?; // XXX: The code of parsing a next port here is quite ugly due to the `get_int` interface. // Please don't be surprised :) let next_port = match u16::try_from(settings.get_int("next_port")?) { Ok(port) => port, // The error will happen when the port number is not in a range of `u16`. Err(_) => { // Returning a custom message is not a good practice, but we can improve it later // when we don't have to depend on `config` crate. return Err(config::ConfigError::Message(String::from( "the next port is not a valid u16", ))); } }; let memcached_url = settings.get_str("memc_url")?; // XXX: The code of parsing a connection timeout here is quite ugly due to the `get_int` // interface. Please don't be surprised :) // Resolves the connection timeout. let timeout = match settings.get_int("conn_timeout") { // If it's a not-found error, we just set it to the default value of 30 seconds. Err(config::ConfigError::NotFound(_)) => 30, // If it's other error, for example, unparseable error, it means that the user intended // to enter the timeout but it just fails. Err(error) => return Err(error), Ok(val) => { match u64::try_from(val) { Ok(val) => val, // The error will happen when the timeout is not in a range of `u64`. Err(_) => { // Returning a custom message is not a good practice, but we can improve // it later when we don't have to depend on `config` crate. return Err(config::ConfigError::Message(String::from( "the connection timeout is not a valid u64", ))); } } } }; // Resolves metrics configuration. let metrics_config = get_metrics_config(&settings); // Note that all of the file reading stuffs should be at the end of the function so that // all the not-file-related stuffs can fail fast. // All config filenames must be given with relative paths to where the server is run. // Otherwise, cfnts will try to open the file while in the incorrect directory. let certs_filename = settings.get_str("tls_cert_file")?; let secret_keys_filename = settings.get_str("tls_key_file")?; let cookie_key_filename = settings.get_str("cookie_key_file")?; let cookie_key = CookieKey::parse(&cookie_key_filename).wrap_err()?; let mut config = KeServerConfig::new( timeout, cookie_key, memcached_url, metrics_config, next_port, ); config.import_tls_certs(&certs_filename).wrap_err()?; config .import_tls_secret_keys(&secret_keys_filename) .wrap_err()?; let addrs = settings.get_array("addr")?; for addr in addrs { // Parse SocketAddr from a string. let sock_addr = addr.to_string().parse().wrap_err()?; config.add_address(sock_addr); } Ok(config) } } ================================================ FILE: src/nts_ke/server/connection.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! NTS-KE server connection. use mio::tcp::{Shutdown, TcpStream}; use rustls::Session; use slog::{debug, error, info}; use std::io::{Read, Write}; use std::sync::{Arc, RwLock}; use crate::cookie::{make_cookie, NTSKeys}; use crate::key_rotator::KeyRotator; use crate::nts_ke::records::gen_key; use crate::nts_ke::records::{ deserialize, process_record, // Functions. serialize, // Records. AeadAlgorithmRecord, // Errors. DeserializeError, EndOfMessageRecord, // Enums. KnownAeadAlgorithm, KnownNextProtocol, NewCookieRecord, NextProtocolRecord, Party, PortRecord, // Structs. ReceivedNtsKeRecordState, // Constants. HEADER_SIZE, }; use super::ke_server::KeServerState; use super::listener::KeServerListener; // response uses the configuration and the keys and computes the response // sent to the client. fn response(keys: NTSKeys, rotator: &Arc>, port: u16) -> Vec { let mut response: Vec = Vec::new(); let next_protocol_record = NextProtocolRecord::from(vec![KnownNextProtocol::Ntpv4]); let aead_record = AeadAlgorithmRecord::from(vec![KnownAeadAlgorithm::AeadAesSivCmac256]); let port_record = PortRecord::new(Party::Server, port); let end_record = EndOfMessageRecord; response.append(&mut serialize(next_protocol_record)); response.append(&mut serialize(aead_record)); let rotor = rotator.read().unwrap(); let (key_id, actual_key) = rotor.latest_key_value(); // According to the spec, if the next protocol is NTPv4, we should send eight cookies to the // client. for _ in 0..8 { let cookie = make_cookie(keys, actual_key.as_ref(), key_id); let cookie_record = NewCookieRecord::from(cookie); response.append(&mut serialize(cookie_record)); } response.append(&mut serialize(port_record)); response.append(&mut serialize(end_record)); response } #[derive(Clone, Copy, Eq, PartialEq)] pub enum KeServerConnState { /// The connection is just connected. The TLS handshake is not done yet. Connected, /// Doing the TLS handshake, TlsHandshaking, /// The TLS handshake is done. It's opened for requests now. Opened, /// The response is sent after getting a good request. ResponseSent, /// The connection is closed. Closed, } /// NTS-KE server TCP connection. pub struct KeServerConn { /// Reference back to the corresponding `KeServer` state. server_state: Arc, /// Kernel TCP stream. tcp_stream: TcpStream, /// The mio token for this connection. token: mio::Token, /// TLS session for this connection. tls_session: rustls::ServerSession, /// The status of the connection. state: KeServerConnState, /// The state of NTS-KE. ntske_state: ReceivedNtsKeRecordState, /// The buffer of NTS-KE Stream. ntske_buffer: Vec, /// Logger. logger: slog::Logger, } impl KeServerConn { pub fn new( tcp_stream: TcpStream, token: mio::Token, listener: &KeServerListener, ) -> KeServerConn { let server_state = listener.state(); // Create a TLS session from a server-wide configuration. let tls_session = rustls::ServerSession::new(&server_state.tls_server_config); // Create a child logger for the connection. let logger = listener .logger() .new(slog::o!("client" => listener.addr().to_string())); let ntske_state = ReceivedNtsKeRecordState { finished: false, next_protocols: Vec::new(), aead_scheme: Vec::new(), cookies: Vec::new(), next_server: None, next_port: None, }; KeServerConn { // Create an `Arc` reference. server_state: server_state.clone(), tcp_stream, tls_session, token, state: KeServerConnState::Connected, ntske_state, ntske_buffer: Vec::new(), logger, } } /// The handler when the connection is ready to ready or write. pub fn ready(&mut self, poll: &mut mio::Poll, event: &mio::Event) { if event.readiness().is_readable() { self.read_ready(); } if event.readiness().is_writable() { self.write_ready(); } if self.state() != KeServerConnState::Closed { // TODO: Fix unwrap later. self.reregister(poll).unwrap(); } } fn read_ready(&mut self) { // If this is the first time that `read_ready` is called, it means that we start reading // some TLS client hello from the client. So we need to change the state to TlsHandshaking. if self.state == KeServerConnState::Connected { self.state = KeServerConnState::TlsHandshaking; } // Read some data from the stream and feed it to the TLS stream. let result = self.tls_session.read_tls(&mut self.tcp_stream); let read_count = match result { Ok(value) => value, Err(error) => { // If it's a WouldBlock, it's not actually an error. So we don't need to close the // connection and return silently. if let std::io::ErrorKind::WouldBlock = error.kind() { return; } // Close the connection on error. error!(self.logger, "read error: {}", error); self.shutdown(); return; } }; // If we reach the end-of-file, just close the connection. if read_count == 0 { info!(self.logger, "eof"); self.shutdown(); return; } // Process newly received TLS messages. let processed = self.tls_session.process_new_packets(); if let Err(error) = processed { error!(self.logger, "cannot process packet: {}", error); self.shutdown(); } let mut buf = Vec::new(); let result = self.tls_session.read_to_end(&mut buf); if let Err(error) = result { error!(self.logger, "read failed: {}", error); self.shutdown(); return; } if !buf.is_empty() { debug!(self.logger, "plaintext read {},", buf.len()); self.ntske_buffer.append(&mut buf); let mut reader = &self.ntske_buffer[..]; // The plaintext is not empty. It means that the handshake is also done. We can change // the state now. if self.state == KeServerConnState::TlsHandshaking { self.state = KeServerConnState::Opened; } let keys = gen_key(&self.tls_session).unwrap(); while !self.ntske_state.finished { // need to read 4 bytes to get the header. if reader.len() < HEADER_SIZE { info!( self.logger, "readable nts-ke stream is not enough to read header" ); self.ntske_buffer = Vec::from(reader); return; } // need to read the body_length to get the body. let body_length = u16::from_be_bytes([reader[2], reader[3]]) as usize; if reader.len() < HEADER_SIZE + body_length { info!( self.logger, "readable nts-ke stream is not enough to read body" ); self.ntske_buffer = Vec::from(reader); return; } // Reconstruct the whole record byte array to let the `records` module deserialize it. let mut record_bytes = vec![0; HEADER_SIZE + body_length]; reader.read_exact(&mut record_bytes).unwrap(); match deserialize(Party::Server, record_bytes.as_slice()) { Ok(record) => { let status = process_record(record, &mut self.ntske_state); match status { Ok(_) => {} Err(err) => { error!(self.logger, "process nts-ke record: {}", err); self.shutdown(); return; } } } Err(DeserializeError::UnknownNotCriticalRecord) => { // If it's not critical, just ignore the error. debug!(self.logger, "unknown record type"); } Err(DeserializeError::UnknownCriticalRecord) => { // TODO: This should propertly handled by sending an Error record. debug!(self.logger, "error: unknown critical record"); self.shutdown(); return; } Err(DeserializeError::Parsing(error)) => { // TODO: This shouldn't be wrapped as a trait object. debug!(self.logger, "error: {}", error); self.shutdown(); return; } } } // We have to make sure that the response is not sent yet. if self.state == KeServerConnState::Opened { // TODO: Fix unwrap later. self.tls_session .write_all(&response( keys, &self.server_state.rotator, self.server_state.config.next_port, )) .unwrap(); // Mark that the response is sent. self.state = KeServerConnState::ResponseSent; } } } fn write_ready(&mut self) { if let Err(error) = self.tls_session.write_tls(&mut self.tcp_stream) { error!(self.logger, "write failed: {}", error); self.shutdown(); } } /// Register the connection with Poll. pub fn register(&self, poll: &mut mio::Poll) -> Result<(), std::io::Error> { poll.register( &self.tcp_stream, self.token, self.interest(), mio::PollOpt::level(), ) } /// Re-register the connection with Poll. pub fn reregister(&self, poll: &mut mio::Poll) -> Result<(), std::io::Error> { poll.reregister( &self.tcp_stream, self.token, self.interest(), mio::PollOpt::level(), ) } fn interest(&self) -> mio::Ready { let mut ready = mio::Ready::empty(); if self.tls_session.wants_read() { ready |= mio::Ready::readable(); } if self.tls_session.wants_write() { ready |= mio::Ready::writable(); } ready } pub fn state(&self) -> KeServerConnState { self.state } pub fn shutdown(&mut self) { // TODO: Fix unwrap later. self.tcp_stream.shutdown(Shutdown::Both).unwrap(); self.state = KeServerConnState::Closed; } } ================================================ FILE: src/nts_ke/server/ke_server.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! NTS-KE server instantiation. use slog::info; use std::sync::{Arc, RwLock}; use crate::key_rotator::periodic_rotate; use crate::key_rotator::KeyRotator; use crate::key_rotator::RotateError; use crate::metrics; use super::config::KeServerConfig; use super::listener::KeServerListener; /// NTS-KE server state that will be shared among listeners. pub(super) struct KeServerState { /// Configuration for the NTS-KE server. // You can see that I don't expand the config's properties here because, by keeping it like // this, we will know what is the config and what is the state. pub(super) config: KeServerConfig, /// Key rotator. Read this property to get latest keys. // The internal state of this rotator can be changed even if the KeServer instance is // immutable. That's because of the nature of RwLock. This property is normally used by // KeServer to read the state only. pub(super) rotator: Arc>, /// TLS server configuration which will be used among listeners. // We use `Arc` here so that every thread can read the config, but the drawback of using `Arc` // is that it uses garbage collection. pub(super) tls_server_config: Arc, } /// NTS-KE server instance. pub struct KeServer { /// State shared among listerners. // We use `Arc` so that all the KeServerListener's can reference back to this object. state: Arc, /// List of listeners associated with the server. /// Each listener is associated with each address in the config. You can check if the server /// already started or not by checking that this vector is empty. // We use `Arc` because the listener will listen in another thread. listeners: Vec>>, } impl KeServer { /// Create a new `KeServer` instance, connect to the Memcached server, and rotate initial keys. /// /// This doesn't start the server yet. It just makes to the state that it's ready to start. /// Please run `start` to start the server. pub fn connect(config: KeServerConfig) -> Result { let rotator = KeyRotator::connect( String::from("/nts/nts-keys"), String::from(config.memcached_url()), // We need to clone all of the following properties because the key rotator also // has to own them. config.cookie_key().clone(), config.logger().clone(), )?; // Putting it in a block just to make it easier to read :) let tls_server_config = { // No client auth for TLS server. let client_auth = rustls::NoClientAuth::new(); // TLS server configuration. let mut server_config = rustls::ServerConfig::new(client_auth); // We support only TLS1.3 server_config.versions = vec![rustls::ProtocolVersion::TLSv1_3]; // Set the certificate chain and its corresponding private key. server_config .set_single_cert( // rustls::ServerConfig wants to own both of them. config.tls_certs.clone(), config.tls_secret_keys[0].clone(), ) .expect("invalid key or certificate"); // According to the NTS specification, ALPN protocol must be "ntske/1". server_config.set_protocols(&[Vec::from("ntske/1".as_bytes())]); server_config }; let state = Arc::new(KeServerState { config, rotator: Arc::new(RwLock::new(rotator)), tls_server_config: Arc::new(tls_server_config), }); Ok(KeServer { state, listeners: Vec::new(), }) } /// Start the server. pub fn start(&mut self) -> Result<(), std::io::Error> { let logger = self.state.config.logger(); // Side-effect. Logging. info!(logger, "initializing keys with memcached"); // Create another reference to the lock so that we can pass it to another thread and // periodically rotate the keys. let mutable_rotator = self.state.rotator.clone(); // Create a new thread and periodically rotate the keys. periodic_rotate(mutable_rotator); // We need to clone the metrics config here because we need to move it to another thread. if let Some(metrics_config) = self.state.config.metrics_config.clone() { info!(logger, "spawning metrics"); // Create a child logger to use inside the metric server. let log_metrics = logger.new(slog::o!("component" => "metrics")); // Start a metric server. std::thread::spawn(move || { metrics::run_metrics(metrics_config, &log_metrics) .expect("metrics could not be run; starting ntp server failed"); }); } // For each address in the config, we will create a listener that will listen on that // address. After the creation, we will create another thread and start listening inside // that thread. for addr in self.state.config.addrs() { // Side-effect. Logging. info!(logger, "starting NTS-KE server over TCP/TLS on {}", addr); // Instantiate a listener. // If there is an error here just return an error immediately so that we don't have to // start a thread for other address. let listener = KeServerListener::bind(*addr, self)?; // It needs to be referenced by this thread and the new thread. let atomic_listener = Arc::new(RwLock::new(listener)); self.listeners.push(atomic_listener); } // Join handles for the listeners. let mut handles = Vec::new(); for listener in self.listeners.iter() { // The listener reference that will be moved into the thread. let cloned_listener = listener.clone(); let handle = std::thread::spawn(move || { // Unwrapping should be fine here because there is no a write lock while we are // trying to lock it and we will wait for the thread to finish before returning // from this `start` method. // // If you don't want to wait for this thread to finish before returning from the // `start` method, you have to look at this `unwrap` and handle it carefully. // // TODO: figure what to do later when the listen fails. cloned_listener.write().unwrap().listen().unwrap(); }); // Add it into the list of listeners. handles.push(handle); } // We need to wait for the listeners to finish. If you don't want to wait for the listeners // anymore, please don't forget to take care an `unwrap` in the thread a few lines above. for handle in handles { // We don't care it's a normal exit or it's a panic from the thread, so we just ignore // the result here. let _ = handle.join(); } Ok(()) } /// Return the state of the server. pub(super) fn state(&self) -> &Arc { &self.state } } ================================================ FILE: src/nts_ke/server/listener.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! NTS-KE server listener. use mio::net::TcpListener; use slog::{error, info}; use std::cmp::Reverse; use std::collections::BinaryHeap; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; use std::time::{Duration, SystemTime}; use crate::cfsock; use super::connection::KeServerConn; use super::connection::KeServerConnState; use super::ke_server::KeServer; use super::ke_server::KeServerState; const LISTENER_MIO_TOKEN_ID: usize = 0; const CONNECTION_MIO_TOKEN_ID_MIN: usize = LISTENER_MIO_TOKEN_ID + 1; // `usize::max_value()` is reserved for mio internal use, so we need to minus one here. const CONNECTION_MIO_TOKEN_ID_MAX: usize = usize::max_value() - 1; /// The token used to associate the mio event with the lister event. const LISTENER_MIO_TOKEN: mio::Token = mio::Token(LISTENER_MIO_TOKEN_ID); /// NTS-KE server internal listener for a specific listened address. /// One listener will correspond to one kernel listening socket. pub struct KeServerListener { /// Reference back to the corresponding `KeServer` state. state: Arc, /// TCP listener for incoming connections. tcp_listener: TcpListener, /// List of connections accepted by this listener. connections: HashMap, /// Deadline indices for connections. // We use `Reverse` because we want a min heap. deadlines: BinaryHeap>, /// The next mio token id for a new connection. next_conn_token_id: usize, /// Address and port that this listener will listen to. addr: SocketAddr, /// Polling object from mio. poll: mio::Poll, /// Logger. logger: slog::Logger, } impl KeServerListener { /// Bind a new listener with the specified address and server. /// /// # Errors /// /// All the errors here are from the kernel which we don't have to know about for now. pub fn bind(addr: SocketAddr, server: &KeServer) -> Result { let state = server.state(); let poll = mio::Poll::new()?; // Create a listening std tcp listener. let std_tcp_listener = cfsock::tcp_listener(&addr)?; // Transform a std tcp listener to a mio tcp listener. let mio_tcp_listener = TcpListener::from_std(std_tcp_listener)?; // Register for the event that the listener is readable. poll.register( &mio_tcp_listener, LISTENER_MIO_TOKEN, mio::Ready::readable(), mio::PollOpt::level(), )?; Ok(KeServerListener { // Create an `Arc` reference. state: state.clone(), tcp_listener: mio_tcp_listener, connections: HashMap::new(), deadlines: BinaryHeap::new(), next_conn_token_id: CONNECTION_MIO_TOKEN_ID_MIN, addr, // In the future, we may want to use the child logger instead the logger itself. logger: state.config.logger().clone(), poll, }) } /// Block the thread and start polling the events. pub fn listen(&mut self) -> Result<(), std::io::Error> { // Holding up to 2048 events. let mut events = mio::Events::with_capacity(2048); loop { // The error returned here is from the kernel select. self.poll.poll(&mut events, None)?; for event in events.iter() { // Close all expired connections. self.close_expired_connections(); let token = event.token(); // If the event is the listener event. if token == LISTENER_MIO_TOKEN { // Start accepting a new connection. if let Err(error) = self.accept() { error!( self.logger, "accept failed unrecoverably with error: {}", error ); } continue; }; // If the event is not the listener event, it must be a connection event. // The connection associated with the token may not exist for some reason. In which // case, we just ignore it. if let Some(connection) = self.connections.get_mut(&token) { connection.ready(&mut self.poll, &event); if connection.state() == KeServerConnState::Closed { self.connections.remove(&token); } } } } } /// Accepting a new connection. This will not block the thread, if it's called after receiving /// the `LISTENER_MIO_TOKEN` event. But it will block, if it's not. fn accept(&mut self) -> Result<(), std::io::Error> { let (tcp_stream, addr) = match self.tcp_listener.accept() { Ok(value) => value, Err(error) => { // If it's WouldBlock, just treat it like a success because there isn't an actual // error. It's just in a non-blocking mode. if error.kind() == std::io::ErrorKind::WouldBlock { return Ok(()); } // If it's not WouldBlock, it's an error. error!( self.logger, "encountered error while accepting connection; err={}", error ); // TODO: I don't understand why we need another tcp listener and register a new // event here. I will figure it out after I finish refactoring everything. self.tcp_listener = TcpListener::bind(&self.addr)?; // TODO: Ignore error first. I wil figure out what to do later if there is an // error. self.poll.register( &self.tcp_listener, LISTENER_MIO_TOKEN, mio::Ready::readable(), mio::PollOpt::level(), )?; // TODO: I will figure why it returns Ok later. return Ok(()); } }; // Successfully accepting a connection. info!(self.logger, "accepting new connection from {}", addr); let token = mio::Token(self.next_conn_token_id); self.increment_next_conn_token_id(); let timeout_duration = Duration::new(self.state.config.timeout(), 0); // If the timeout is so large that we cannot put it in SystemTime, we can assume that // it doesn't have a timeout and just don't add it into the map. if let Some(timeout_systime) = SystemTime::now().checked_add(timeout_duration) { self.deadlines.push(Reverse((timeout_systime, token))); } // Create a new connection instance. let connection = KeServerConn::new(tcp_stream, token, self); // TODO: Fix the unwrap later. connection.register(&mut self.poll).unwrap(); self.connections.insert(token, connection); Ok(()) } /// Increment next_conn_token_id. fn increment_next_conn_token_id(&mut self) { match self.next_conn_token_id.checked_add(1) { Some(value) => self.next_conn_token_id = value, // If it overflows just set it to the minimum value. None => self.next_conn_token_id = CONNECTION_MIO_TOKEN_ID_MIN, } // If it exceeds the maximum, we also set it to the minimum value. if self.next_conn_token_id > CONNECTION_MIO_TOKEN_ID_MAX { self.next_conn_token_id = CONNECTION_MIO_TOKEN_ID_MIN; } } /// Closes the expired timeouts, looping until they are all gone. /// We remove the timeout from the heap, and kill the connection if it exists. fn close_expired_connections(&mut self) { let now = SystemTime::now(); while let Some(earliest) = self.deadlines.peek() { let Reverse((deadline, token)) = earliest; if deadline < &now { // If the deadline is already elapsed, close the connection and pop the heap. // The connection associated with the token may not exist because, when we close // the connection, it's not possible to find an entry in the heap. In which case, // we can just pop the deadline heap. if let Some(mut connection) = self.connections.remove(token) { error!(self.logger, "forcible shutdown after timeout"); connection.shutdown(); } self.deadlines.pop(); // In this case, this means that there may be more elapsed deadline. Continue the // loop. } else { // If not, it means there is no more elapsed deadline in the heap. So we can just // stop the loop. break; } } } /// Return the state of the corresponding server. pub(super) fn state(&self) -> &Arc { &self.state } /// Return the logger of this listener. pub(super) fn logger(&self) -> &slog::Logger { &self.logger } /// Return the address-port of this listener. pub(super) fn addr(&self) -> &SocketAddr { &self.addr } } ================================================ FILE: src/nts_ke/server/mod.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! NTS-KE server implementation. mod config; mod connection; mod ke_server; mod listener; // We expose only two structs: KeServer and KeServerConfig. KeServer is used to run an instant of // the NTS-KE server and KeServerConfig is used to instantiate KeServer. pub use self::config::KeServerConfig; pub use self::ke_server::KeServer; ================================================ FILE: src/sub_command/client.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! The client subcommand. use slog::debug; use std::fs; use std::io::BufReader; use std::process; use rustls::{internal::pemfile::certs, Certificate}; use crate::error::WrapError; use crate::ntp::client::run_nts_ntp_client; use crate::nts_ke::client::run_nts_ke_client; #[derive(Debug)] pub struct ClientConfig { pub host: String, pub port: Option, pub trusted_cert: Option, pub use_ipv4: Option, } pub fn load_tls_certs(path: String) -> Result, config::ConfigError> { certs(&mut BufReader::new(fs::File::open(&path).wrap_err()?)).map_err(|()| { config::ConfigError::Message(format!("could not load certificate from {}", &path)) }) } /// The entry point of `client`. pub fn run(matches: &clap::ArgMatches<'_>) { // This should return the clone of `logger` in the main function. let logger = slog_scope::logger(); let host = matches.value_of("host").map(String::from).unwrap(); let port = matches.value_of("port").map(String::from); let cert_file = matches.value_of("cert").map(String::from); // By default, use_ipv4 is None (no preference for using either ipv4 or ipv6 // so client sniffs which one to use based on support) // However, if a user specifies the ipv4 flag, we set use_ipv4 = Some(true) // If they specify ipv6 (only one can be specified as they are mutually exclusive // args), set use_ipv4 = Some(false) let ipv4 = matches.is_present("ipv4"); let mut use_ipv4 = None; if ipv4 { use_ipv4 = Some(true); } else { // Now need to check whether ipv6 is being used, since ipv4 has not been mandated if matches.is_present("ipv6") { use_ipv4 = Some(false); } } let mut trusted_cert = None; if let Some(file) = cert_file { if let Ok(certs) = load_tls_certs(file) { trusted_cert = Some(certs[0].clone()); } } let client_config = ClientConfig { host, port, trusted_cert, use_ipv4, }; let res = run_nts_ke_client(&logger, client_config); if let Err(err) = res { eprintln!("failure of tls stage: {}", err); process::exit(1) } let state = res.unwrap(); debug!(logger, "running UDP client with state {:x?}", state); let res = run_nts_ntp_client(&logger, state); match res { Err(err) => { eprintln!("failure of client: {}", err); process::exit(1) } Ok(result) => { println!("stratum: {:}", result.stratum); println!("offset: {:.6}", result.time_diff); } } } ================================================ FILE: src/sub_command/ke_server.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! The ke-server subcommand. use std::process; use crate::nts_ke::server::{KeServer, KeServerConfig}; /// Get a configuration file path for `ke-server`. /// /// If the path is not specified, the system-wide configuration file (/etc/cfnts/ke-server.config) /// will be used instead. /// fn resolve_config_filename(matches: &clap::ArgMatches<'_>) -> String { match matches.value_of("configfile") { // If the config file is specified in the arguments, just use it. Some(filename) => String::from(filename), // If not, use the system-wide configuration file. None => String::from("/etc/cfnts/ke-server.config"), } } /// The entry point of `ke-server`. pub fn run(matches: &clap::ArgMatches<'_>) { // This should return the clone of `logger` in the main function. let global_logger = slog_scope::logger(); // Get the config file path. let filename = resolve_config_filename(matches); let mut config = match KeServerConfig::parse(&filename) { Ok(val) => val, // If there is an error, display it. Err(err) => { eprintln!("{}", err); process::exit(1); } }; let logger = global_logger.new(slog::o!("component" => "nts_ke")); // Let the parsed config use the child logger of the global logger. config.set_logger(logger); // Try to connect to the Memcached server. let mut server = match KeServer::connect(config) { Ok(server) => server, Err(_error) => { // Disable the log for now because the Error trait is not implemented for // RotateError yet. // eprintln!("starting NTS-KE server failed: {}", error); process::exit(1); } }; // Start listening for incoming connections. if let Err(error) = server.start() { eprintln!("starting NTS-KE server failed: {}", error); process::exit(1); } } ================================================ FILE: src/sub_command/mod.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! Subcommand collections. pub mod client; pub mod ke_server; pub mod ntp_server; ================================================ FILE: src/sub_command/ntp_server.rs ================================================ // This file is part of cfnts. // Copyright (c) 2019, Cloudflare. All rights reserved. // See LICENSE for licensing information. //! The ntp-server subcommand. use std::process; use crate::ntp::server::start_ntp_server; use crate::ntp::server::NtpServerConfig; /// Get a configuration file path for `ntp-server`. /// /// If the path is not specified, the system-wide configuration file (/etc/cfnts/ntp-server.config) /// will be used instead. /// fn resolve_config_filename(matches: &clap::ArgMatches<'_>) -> String { match matches.value_of("configfile") { // If the config file is specified in the arguments, just use it. Some(filename) => String::from(filename), // If not, use the system-wide configuration file. None => String::from("/etc/cfnts/ntp-server.config"), } } /// The entry point of `ntp-server`. pub fn run(matches: &clap::ArgMatches<'_>) { // This should return the clone of `logger` in the main function. let global_logger = slog_scope::logger(); // Get the config file path. let filename = resolve_config_filename(matches); let mut config = match NtpServerConfig::parse(&filename) { Ok(val) => val, // If there is an error, display it. Err(err) => { eprintln!("{}", err); process::exit(1); } }; let logger = global_logger.new(slog::o!("component" => "ntp")); // Let the parsed config use the child logger of the global logger. config.set_logger(logger); if let Err(err) = start_ntp_server(config) { eprintln!("starting NTP server failed: {}", err); process::exit(1); } } ================================================ FILE: tests/ca-key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCzTB9ETn6RgGHT EXkvXtUxCRtN5oz8eh1hD98OBOYrqC9gpw90xurkpvrSKA/XGi2er+b+fDaMZnvO rUAmO5tkBeUv5VRArKAW1lTocTTFCXbbS1pMd4fxCePXnDed81MWFThYLr9zJ8KU eamYMBWq6lziAynTT9+bVaj5zkLC23u9EUqPFn8Kg3hdfLE5FPzeWqYREVrst++f npjrt6ZWlHohA6P6BAplubBTKtcOcyDT1Y8Wg7OxgLex+gq5X+4YxzpuvPzRL0uc l4210FhPY+55zJ4Vw6lqQmY7lSloQQO2PbtkRvfpphy7J1aPaVpWXvVK2mv4tZtX zplOK3Uf1m4hVCA/fN1A5w89RCzdOacYSEvvFRhpFo4Iw0L3u2jvO71Rxpe+ATZk Kzhk0TnwyRCQGYGYjRsNRAnGHbe10fPaT2NoBDmojg4S1LF6plwYYxTXvUsIWo9c KizMAYB6p6jCIcJUupulyChnSyTxvLOLKJr0YuU3ug4WEH1fw2JVgYi4X8TeGRgz LqBA9NudPJZjXh+fCcmdfIqNpuVujWJGqFjzG6NyixGJ5SqnIByFJd1UhF5UgJPL yseSrAn1Zqk4wKDtjPnahgDIqoLNjBh8vc2jbwTShwWWxnZK2iA+VzG4Y9l7I1Fp ANkJp+oug6tiCiBwYzMzP6rJjDBI6wIDAQABAoICAD3Tpwh/5Mc5tQH6iYZbNjrF gCPZt44sccsRlQIZkGFHiqbSlNLY8RDNv7oOVIABJ/ALiiUBIjJB+LlpJrDIZyoT mldsxiPTIxUc7YSF3QOA4vp1vnqV0Uu99FJaLReLW4BG6voFjMEh2cgnN+Mh2abp UAQjwR178oh2/mC9zmmxE7c7qjEzObWfZjcek2IyqYvnSFKkYG02dCvfna3S00oR wxd1UOsaz5cKdBIJuMTj0FMb1k6WNbWkxDNcHKyVtt3WfYDILInZvEIQRK6IXJtr w0U+2Nh6cwYQRX6QTgoEOUpzeRX4Hu7z9/5Vb1TeqGcWMZGRRiAqR5n8xQKem7E9 40uQdTr1GY+89x5G/AkzD/IvKCrVZLBYjzXwNZmIztoVMu0bWDwijrR2c17lQHvA hWz4DOaONxRBxCbva7dG4FZZRj3F1q3aN7cpvFMRP7QzVtkDz1k7j8q01O1hlzY8 ezGXcHeu60Dy+/TcLvJVwxJ6/uaGcwTpsS88/ybVlJ2V24JjcoFE5YoiJEBlLW93 UBHGmaS9zd8HAIkbNugpQCHFJRVfaaINLbchpV6g/ETlGyUPRcXMZmtpuqm72esu eWyYxoPxQ99u6Y4zoIRqx/H2eDmhxluLLwstIMA3uFD0AgcLpDHOAOlvY6QWkQuE 7TkuPcbAwGo6J3SSbBpJAoIBAQDhyzGB5ZNbVhMlbYhQiBTgQ0twBn9xLKH3sQiS iIru/IHJxBAgz/Cg3x7CQqoz2HgZw8+kWYmwwt/icFEnCv35jErwSelpGHLx74cm ZpzEM6hPOjtDG5Afqc6YOr5dKTHv9Gidpz0uQAKZ20awHUmkLNsOy6zfB9RhMT5y Bx8mictEFweOjbrn7qSwm6cssoZcoqbU52sgJ/2JlSdqBbajSFkO0hPerZh/Z/x6 F/xNQR11DCsBHPMRlvUhxvk4sAjUEPhp6Pw1NUJvdsewQmIdNAE3cH+p/w4uVmvo u+qY8X5+cz097rq9ElZxOQqaGaGe7Pvid1Y4u8NC/BjDefoPAoIBAQDLSI6ywzQK L8YzpiIhE5Q9VDLlolv3+s02US+MNN8djMI5D9/2tvaoXaA5ax3OcvYyv0WLTddX WOiCWAe14gIX3jTPHWbzzLfE3AYQDu7P7C3u6bripcRMVIHQ4z7Juk2xrFQjsW9Y woq4nc6CrVJhcH5koFpn+CEU/A0vzmWfw2w/5rSZ7Mi74upPDElKwx6L8wth1sGK bKhD4+tMbVA3fYu2PqxY8LcFjyeh9mDZJSyFSIi4Itb1e1QPCD2TQTHiQdo0Lap0 gk5ylCQaeLJDbFGVioeUfmfngvNP3n8ye/bc3h6OrM4sAz6lXMhCJfB6TE2IuAKE vKmmry5Guk9lAoIBAHTigQBjXcLcbhDkALrflx749yZI1tQ5bKcSSAPDF1jb8jwG eOrjegdtOTkK1Zz9JD8CNI05pKOSXd+UkQ4LDKqQS4LUYDX9aBOCEY55dBHFRA2v cVot/I/HkaEQV9dWKfmzpixmlK9Kh44qCw/EOYj5h3TDTvwty2181nyk3yVOE6Ft 4oWTLPw/d5XNHd9vk0qFEKQKIFSHHyKHyd2Ck6c3HpMjgRG2/8iEhhiWLg+3843R /LkYyWODp+YSYJVN22QcXNxGtbi9l2SoMns2AiBn+XE/lXblB+xI5JeYH7uI2BiR g1R6LsUNpx35j1lyh04EE+iKKmI4IL6eThtzG1UCggEBAMCZAfoET+3GzbZplLRZ 5H0mpQJEDXapPHxV9wKTpUBN+EYv8DXDq3ZhHkjIX/kVmoUCC1WsbnXnWoMD/Goq s2kBsm74oG4ka4gsHeJhA4ojbnGJKPNLsuvOtR+/7eEajjnj1+PpXGFwEBZSDTJq HD8NYfLcqksPH+jN1YCRwF7ZvFnerwWW/ahlmTFDpr0amHpnz0TnP39y6wlHi8th Vjr8y73jK08o4X5230noMGILgl7VFhO/joIOUtnbKNu3TRfc5GvDSFgSjVipWntq FxsiKTnRghsCmFcUDoqBd2nRYVZpa/Ipbzzr5hKuEV36rBhy6pK6JEi2ptWx69o+ 8rECggEBALzsK4L46sHkJOl15yxjXPV6ZmtxhHnMCPLsNrKGJZRti53JUqCrWF+v 9SBOCLnUmwijY5tbmi+CdQkpnV5VxF4nmN65KH8BonRL2pKDnGionqdqBlr7hupj TdLlhQqTJQGLsJcsRhGiLbjyLuJMDvtQaGC7F3vPQwYqdsj5iPRFJbLS7OPkcpud Okfm6GhBCOaMjBM2WRgWJwmAnwt9t/YiU1DhPKrUCPb5pzYXEA1DC0IWbDISLCTt SpbR/hX6jw/IhuvCQ0vPP+4NeL9GjVNo8iF6NYIwV9swu22yQhtklooDiuQ8fzCw xnbbOtvjxJ1sG5mW9Dblwe3GAIoI35c= -----END PRIVATE KEY----- ================================================ FILE: tests/ca.csr ================================================ -----BEGIN CERTIFICATE REQUEST----- MIIEizCCAnMCAQAwRjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH DA1TYW4gRnJhbmNpc2NvMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3 DQEBAQUAA4ICDwAwggIKAoICAQCzTB9ETn6RgGHTEXkvXtUxCRtN5oz8eh1hD98O BOYrqC9gpw90xurkpvrSKA/XGi2er+b+fDaMZnvOrUAmO5tkBeUv5VRArKAW1lTo cTTFCXbbS1pMd4fxCePXnDed81MWFThYLr9zJ8KUeamYMBWq6lziAynTT9+bVaj5 zkLC23u9EUqPFn8Kg3hdfLE5FPzeWqYREVrst++fnpjrt6ZWlHohA6P6BAplubBT KtcOcyDT1Y8Wg7OxgLex+gq5X+4YxzpuvPzRL0ucl4210FhPY+55zJ4Vw6lqQmY7 lSloQQO2PbtkRvfpphy7J1aPaVpWXvVK2mv4tZtXzplOK3Uf1m4hVCA/fN1A5w89 RCzdOacYSEvvFRhpFo4Iw0L3u2jvO71Rxpe+ATZkKzhk0TnwyRCQGYGYjRsNRAnG Hbe10fPaT2NoBDmojg4S1LF6plwYYxTXvUsIWo9cKizMAYB6p6jCIcJUupulyChn SyTxvLOLKJr0YuU3ug4WEH1fw2JVgYi4X8TeGRgzLqBA9NudPJZjXh+fCcmdfIqN puVujWJGqFjzG6NyixGJ5SqnIByFJd1UhF5UgJPLyseSrAn1Zqk4wKDtjPnahgDI qoLNjBh8vc2jbwTShwWWxnZK2iA+VzG4Y9l7I1FpANkJp+oug6tiCiBwYzMzP6rJ jDBI6wIDAQABoAAwDQYJKoZIhvcNAQELBQADggIBAI+EU8ck+zgruCBjtfzqhkJ9 PtHmaRMG5ziq5tUFzHe3O++N09DKt0R+mvKxYLDDj9w61qPy0MH1UDsWaftE/LWE ujIACIbg6Ium3iU4KViQUXMoTYveLjhh8f2i+IKDSEnORgmDBwX6Xg153SZNLZHh hn8xiJ34bJRrsyOJM1zwGjXiD6ikNALv7OLtL45H+mpdk6BzpcJEUKBdGpa1pp1p iCQwPbvkA/vi+OOAaUuAafrt2RaPLVOpHgvNj7PlWX6qNmUe52tTFegBIb30qtrL DS+yqbFeHcVQ7ypV2hbqOs3uumFMUBMM0yDPPopBb0xINfKd+IOm7uVwLrHUm3sU kOzEJRdYN6n0LQFjbpQnAM7nhu31RCtsyeUStAfdmlQCetg0vhmir0hpkMLTg/ln /bIcDx5S2ODVS8tlfD5ggugHThdxrC2xjfvSlOUu9y9zQZnxlOscwQW9vJDKn9mV zWXqf4SjJks1yEg57XG9WkzchiEvtQpQu2d0fZpW4+O8qQlUBIxXc3di4whHYx0j WWxO27NFPSlTZb6TrjZW4N02vfWMbczWOqrkKBp61EpZYg6rDsZQd7t0UYTjIZDz El1QyEbof28R9bskiwC7EGwR9DguodFH7l2K+DIjpl8FxysLFxMU2YGJOu81j5Pn odps4ahbCad6c5cgkeJW -----END CERTIFICATE REQUEST----- ================================================ FILE: tests/ca.pem ================================================ -----BEGIN CERTIFICATE----- MIIFEzCCAvsCFEXBseLI9/DDsUJVNhy2Nn2ORSZFMA0GCSqGSIb3DQEBCwUAMEYx CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNj bzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDQwOTA3MjYyNFoXDTM0MDQwNzA3 MjYyNFowRjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4g RnJhbmNpc2NvMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUA A4ICDwAwggIKAoICAQCzTB9ETn6RgGHTEXkvXtUxCRtN5oz8eh1hD98OBOYrqC9g pw90xurkpvrSKA/XGi2er+b+fDaMZnvOrUAmO5tkBeUv5VRArKAW1lTocTTFCXbb S1pMd4fxCePXnDed81MWFThYLr9zJ8KUeamYMBWq6lziAynTT9+bVaj5zkLC23u9 EUqPFn8Kg3hdfLE5FPzeWqYREVrst++fnpjrt6ZWlHohA6P6BAplubBTKtcOcyDT 1Y8Wg7OxgLex+gq5X+4YxzpuvPzRL0ucl4210FhPY+55zJ4Vw6lqQmY7lSloQQO2 PbtkRvfpphy7J1aPaVpWXvVK2mv4tZtXzplOK3Uf1m4hVCA/fN1A5w89RCzdOacY SEvvFRhpFo4Iw0L3u2jvO71Rxpe+ATZkKzhk0TnwyRCQGYGYjRsNRAnGHbe10fPa T2NoBDmojg4S1LF6plwYYxTXvUsIWo9cKizMAYB6p6jCIcJUupulyChnSyTxvLOL KJr0YuU3ug4WEH1fw2JVgYi4X8TeGRgzLqBA9NudPJZjXh+fCcmdfIqNpuVujWJG qFjzG6NyixGJ5SqnIByFJd1UhF5UgJPLyseSrAn1Zqk4wKDtjPnahgDIqoLNjBh8 vc2jbwTShwWWxnZK2iA+VzG4Y9l7I1FpANkJp+oug6tiCiBwYzMzP6rJjDBI6wID AQABMA0GCSqGSIb3DQEBCwUAA4ICAQCmo1FG5Dudyy7Z0MgHY/dHe9EHWMl8FPIK zRxFrsAUivNMaXG+rUmsPgd0tNUdqEYQOpDYyu61ayo9dZUfjfoiePp/h6jiZrUa OxWtC53Em/UDoVz/hElRFwOYCz5O3ZQRC+c/CjSb+hsB93gi3bJIq3mIGCe9+jf1 YD2GkaD99V5gZq5U5cTGsD9rxdAOT4AMEsxsUAUVULzhA+nQw4uqxFDe2AC8ZY9j AerCXu5BiLDcB3YnwnHaZ7MXbpWROSYQCmgojxUoiycAnZNJssFF2c/PVrRI3z0J vKhO7ViGs+JOqfp5jdgZO0SdKYT+n/TpF3Eqn/ugcXbyBBqS+Vj9liwTKNLqw7Um GWPOXoczYOp0iIv7qH6HbqpmZgwt4j1Xn7oMkZMv2fjYFCIS4PjifTVBaIm6FBg8 XE0wGlWoMQtfxy56lPNub8Rnq6SyYtKJZfap8ukaPgL6mMJ7RYL1tjiHM9FRXS+7 MrDo1bsQUoCqh6ZmWMyMfGycDflZg1DuAwwwJ06OmEBSqvkZ1sjZi/LGIAwKC2cw YsIJsKNw/rdE+ph7reH9RiDLJe+I2WehDZCqZ3dA5d8NjK2wGEo6G54YxKARpndl AlUeB/KJNFwLU74FPL2Jn9yfXTcqJbGs+AIpAu/OtwhPEkIizoeRO0cwqhURa2f/ q5Qa16K2Bw== -----END CERTIFICATE----- ================================================ FILE: tests/chain.pem ================================================ -----BEGIN CERTIFICATE----- MIICoDCCAkegAwIBAgIUW5W4GNGYJwryph3KHKkdLaeFdvMwCgYIKoZIzj0EAwIw gY4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T YW4gRnJhbmNpc2NvMRgwFgYDVQQKEw9IYXBweUNlcnQsIEluYy4xHzAdBgNVBAsT FkhhcHB5Q2VydCBJbnRlcm1lZGlhdGUxFzAVBgNVBAMTDihkZXYgdXNlIG9ubHkp MCAXDTI0MDQwOTA3MjEwMFoYDzIxMjQwMzE2MDcyMTAwWjB2MQswCQYDVQQGEwJV UzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGDAWBgNVBAoT D0Nsb3VkZmxhcmUgdGVzdDEUMBIGA1UECxMLQ3J5cHRvIHRlYW0xEjAQBgNVBAMT CWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHG0wqxNChNemkM/ Aw05RBB0vs9adyC1tIm+pobqB0T6T50HN59NeDxMsfeALWBN/i23FKphwdNGIzO3 NkNhMg2jgZcwgZQwDgYDVR0PAQH/BAQDAgGGMAwGA1UdEwEB/wQCMAAwHQYDVR0O BBYEFJw0MuxNY1BtEB3oNMbPiwxDNrqZMB8GA1UdIwQYMBaAFOu3ANbLR8TJecGR AAuxL7juFmVyMDQGA1UdEQQtMCuCBnNlcnZlcoIJbG9jYWxob3N0gglib2d1cy5j b22CCyoubG9jYWxob3N0MAoGCCqGSM49BAMCA0cAMEQCIAJ0Os/bYUfH6nPO8f1E vVateUJXKaPuS6jD3i0eWQYbAiAyC4TPPr4S0wUXGf6RYwbTPG3sAvGAnuxpxlqB P0sZrQ== -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIID3zCCAcegAwIBAgIUalsEFgf4uPaULSbhh3By7ebBUSQwDQYJKoZIhvcNAQEN BQAwRjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh bmNpc2NvMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjQwNDA5MDcyMTAwWhgPMjEy NDAzMTYwNzIxMDBaMIGOMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p YTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEYMBYGA1UEChMPSGFwcHlDZXJ0LCBJ bmMuMR8wHQYDVQQLExZIYXBweUNlcnQgSW50ZXJtZWRpYXRlMRcwFQYDVQQDEw4o ZGV2IHVzZSBvbmx5KTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJABtGVq08uw OV99fDjxN38bWMAe2SeZeBHxF/TEsjtc7jvZtcFv6SECzu9qq6ktUsymCtSDYnP1 bJ2VZLnEQ4SjRTBDMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEA MB0GA1UdDgQWBBTrtwDWy0fEyXnBkQALsS+47hZlcjANBgkqhkiG9w0BAQ0FAAOC AgEAl9aRyMaYMJSyWAI4BZmPSxzs8cIVa7lOnCLI3AevDOw4AEXTwK6VFZaehuWB Vfodav9LrNQ8m/Po3K7AQwQYBghLwaQu7ISlI4pIGUeAZaIo90Bv0H2BJb3foHvi 4+RI+CjVHXucKkgNU998RG6edwDsmdp963kKs3/AiU0vUgyUbuEzhzH4Dgqzt99w 0ekf33fDGRvJ6k45oWZ7gkeT1gcbhCFafQJrMRKgoXcxPxwGxvn+usSd0EUuvMeF soQJYZ/nMtSahC5qR2TRDunsUAtDtWk7LhdKQF9c+z8IHupxga8x1qxsAcu0abae NQFUwoyEVUxafMuUdPS8D/br+A2RxaiohAISHLCT7gZVxDkGAT6j8z+nrpvQU/UN WMLQizGjv7qxXBNHCzo62mZGoZEJNdDP+FzNBdZ3cvYf16t7AWGd7X95I4gj+Muu J+/VqdqDd17JFTvZ9czc05AsksPwxTMYrXRqfcn9CZeMqinr0kcJ727WtRU6I5wW 52G21D52BCrBZJfTvh+SEoZyTlvV43mt7VIRxB+xxd3zP3OH7a0amTH9f33O6E9u 23r00qyBiluwLGnD2Jca+8AhwsP9uDH8MkTlidPQXGwjrkVhs5+uKC9Zug7G0jEs qzjuEdhe2UGCaK30J/AMxR3brzIDTTxdJAwn7ZnvqQ6+7YU= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFEzCCAvsCFEXBseLI9/DDsUJVNhy2Nn2ORSZFMA0GCSqGSIb3DQEBCwUAMEYx CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNj bzESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDQwOTA3MjYyNFoXDTM0MDQwNzA3 MjYyNFowRjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4g RnJhbmNpc2NvMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUA A4ICDwAwggIKAoICAQCzTB9ETn6RgGHTEXkvXtUxCRtN5oz8eh1hD98OBOYrqC9g pw90xurkpvrSKA/XGi2er+b+fDaMZnvOrUAmO5tkBeUv5VRArKAW1lTocTTFCXbb S1pMd4fxCePXnDed81MWFThYLr9zJ8KUeamYMBWq6lziAynTT9+bVaj5zkLC23u9 EUqPFn8Kg3hdfLE5FPzeWqYREVrst++fnpjrt6ZWlHohA6P6BAplubBTKtcOcyDT 1Y8Wg7OxgLex+gq5X+4YxzpuvPzRL0ucl4210FhPY+55zJ4Vw6lqQmY7lSloQQO2 PbtkRvfpphy7J1aPaVpWXvVK2mv4tZtXzplOK3Uf1m4hVCA/fN1A5w89RCzdOacY SEvvFRhpFo4Iw0L3u2jvO71Rxpe+ATZkKzhk0TnwyRCQGYGYjRsNRAnGHbe10fPa T2NoBDmojg4S1LF6plwYYxTXvUsIWo9cKizMAYB6p6jCIcJUupulyChnSyTxvLOL KJr0YuU3ug4WEH1fw2JVgYi4X8TeGRgzLqBA9NudPJZjXh+fCcmdfIqNpuVujWJG qFjzG6NyixGJ5SqnIByFJd1UhF5UgJPLyseSrAn1Zqk4wKDtjPnahgDIqoLNjBh8 vc2jbwTShwWWxnZK2iA+VzG4Y9l7I1FpANkJp+oug6tiCiBwYzMzP6rJjDBI6wID AQABMA0GCSqGSIb3DQEBCwUAA4ICAQCmo1FG5Dudyy7Z0MgHY/dHe9EHWMl8FPIK zRxFrsAUivNMaXG+rUmsPgd0tNUdqEYQOpDYyu61ayo9dZUfjfoiePp/h6jiZrUa OxWtC53Em/UDoVz/hElRFwOYCz5O3ZQRC+c/CjSb+hsB93gi3bJIq3mIGCe9+jf1 YD2GkaD99V5gZq5U5cTGsD9rxdAOT4AMEsxsUAUVULzhA+nQw4uqxFDe2AC8ZY9j AerCXu5BiLDcB3YnwnHaZ7MXbpWROSYQCmgojxUoiycAnZNJssFF2c/PVrRI3z0J vKhO7ViGs+JOqfp5jdgZO0SdKYT+n/TpF3Eqn/ugcXbyBBqS+Vj9liwTKNLqw7Um GWPOXoczYOp0iIv7qH6HbqpmZgwt4j1Xn7oMkZMv2fjYFCIS4PjifTVBaIm6FBg8 XE0wGlWoMQtfxy56lPNub8Rnq6SyYtKJZfap8ukaPgL6mMJ7RYL1tjiHM9FRXS+7 MrDo1bsQUoCqh6ZmWMyMfGycDflZg1DuAwwwJ06OmEBSqvkZ1sjZi/LGIAwKC2cw YsIJsKNw/rdE+ph7reH9RiDLJe+I2WehDZCqZ3dA5d8NjK2wGEo6G54YxKARpndl AlUeB/KJNFwLU74FPL2Jn9yfXTcqJbGs+AIpAu/OtwhPEkIizoeRO0cwqhURa2f/ q5Qa16K2Bw== -----END CERTIFICATE----- ================================================ FILE: tests/cookie.key ================================================ Td>!鼽v ================================================ FILE: tests/generate.sh ================================================ openssl req -newkey rsa:4096 -keyout ca-key.pem -out ca.csr -days 3650 -nodes -subj "/C=US/ST=CA/L=San Francisco/CN=localhost" openssl x509 -in ca.csr -out ca.pem -req -signkey ca-key.pem -days 3650 cfssl gencert -config=int-config.json -ca=ca.pem -ca-key=ca-key.pem intermediate.json | cfssljson -bare intermediate cfssl gencert -config=test-config.json -ca intermediate.pem -ca-key intermediate-key.pem test.json | cfssljson -bare tls openssl pkcs8 -topk8 -nocrypt -in tls-key.pem -out tls-pkcs8.pem cat tls.pem intermediate.pem ca.pem > chain.pem ================================================ FILE: tests/int-config.json ================================================ { "signing": { "default": { "ca_constraint": { "is_ca": true, "max_path_len": 0, "max_path_len_zero": true }, "expiry": "876000h", "usages": [ "digital signature", "cert sign", "crl sign", "signing" ] } } } ================================================ FILE: tests/intermediate-key.pem ================================================ -----BEGIN EC PRIVATE KEY----- MHcCAQEEIBTkESHvag8yy5dO8xza5Zo52TRDDQgmqWBMpWsBRjmPoAoGCCqGSM49 AwEHoUQDQgAEkAG0ZWrTy7A5X318OPE3fxtYwB7ZJ5l4EfEX9MSyO1zuO9m1wW/p IQLO72qrqS1SzKYK1INic/VsnZVkucRDhA== -----END EC PRIVATE KEY----- ================================================ FILE: tests/intermediate.csr ================================================ -----BEGIN CERTIFICATE REQUEST----- MIIBSTCB8QIBADCBjjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEx FjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGDAWBgNVBAoTD0hhcHB5Q2VydCwgSW5j LjEfMB0GA1UECxMWSGFwcHlDZXJ0IEludGVybWVkaWF0ZTEXMBUGA1UEAxMOKGRl diB1c2Ugb25seSkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASQAbRlatPLsDlf fXw48Td/G1jAHtknmXgR8Rf0xLI7XO472bXBb+khAs7vaqupLVLMpgrUg2Jz9Wyd lWS5xEOEoAAwCgYIKoZIzj0EAwIDRwAwRAIgWi05qNqepbhZRiPAK5zhqpbGWOXQ 2V+lganS10JrHRkCIBlcIxyKDSAdsVDbAHe8Pk/V7bqeSzEMH9LkOQi8Xq2O -----END CERTIFICATE REQUEST----- ================================================ FILE: tests/intermediate.json ================================================ { "key": { "algo": "ecdsa", "size": 256 }, "names": [ { "C": "US", "L": "San Francisco", "ST": "California", "O": "HappyCert, Inc.", "OU": "HappyCert Intermediate" } ], "cn": "(dev use only)" } ================================================ FILE: tests/intermediate.pem ================================================ -----BEGIN CERTIFICATE----- MIID3zCCAcegAwIBAgIUalsEFgf4uPaULSbhh3By7ebBUSQwDQYJKoZIhvcNAQEN BQAwRjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh bmNpc2NvMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMjQwNDA5MDcyMTAwWhgPMjEy NDAzMTYwNzIxMDBaMIGOMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p YTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEYMBYGA1UEChMPSGFwcHlDZXJ0LCBJ bmMuMR8wHQYDVQQLExZIYXBweUNlcnQgSW50ZXJtZWRpYXRlMRcwFQYDVQQDEw4o ZGV2IHVzZSBvbmx5KTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJABtGVq08uw OV99fDjxN38bWMAe2SeZeBHxF/TEsjtc7jvZtcFv6SECzu9qq6ktUsymCtSDYnP1 bJ2VZLnEQ4SjRTBDMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEA MB0GA1UdDgQWBBTrtwDWy0fEyXnBkQALsS+47hZlcjANBgkqhkiG9w0BAQ0FAAOC AgEAl9aRyMaYMJSyWAI4BZmPSxzs8cIVa7lOnCLI3AevDOw4AEXTwK6VFZaehuWB Vfodav9LrNQ8m/Po3K7AQwQYBghLwaQu7ISlI4pIGUeAZaIo90Bv0H2BJb3foHvi 4+RI+CjVHXucKkgNU998RG6edwDsmdp963kKs3/AiU0vUgyUbuEzhzH4Dgqzt99w 0ekf33fDGRvJ6k45oWZ7gkeT1gcbhCFafQJrMRKgoXcxPxwGxvn+usSd0EUuvMeF soQJYZ/nMtSahC5qR2TRDunsUAtDtWk7LhdKQF9c+z8IHupxga8x1qxsAcu0abae NQFUwoyEVUxafMuUdPS8D/br+A2RxaiohAISHLCT7gZVxDkGAT6j8z+nrpvQU/UN WMLQizGjv7qxXBNHCzo62mZGoZEJNdDP+FzNBdZ3cvYf16t7AWGd7X95I4gj+Muu J+/VqdqDd17JFTvZ9czc05AsksPwxTMYrXRqfcn9CZeMqinr0kcJ727WtRU6I5wW 52G21D52BCrBZJfTvh+SEoZyTlvV43mt7VIRxB+xxd3zP3OH7a0amTH9f33O6E9u 23r00qyBiluwLGnD2Jca+8AhwsP9uDH8MkTlidPQXGwjrkVhs5+uKC9Zug7G0jEs qzjuEdhe2UGCaK30J/AMxR3brzIDTTxdJAwn7ZnvqQ6+7YU= -----END CERTIFICATE----- ================================================ FILE: tests/ntp-config.yaml ================================================ addr: - "0.0.0.0:123" - "0.0.0.0:789" - "[::]:123" cookie_key_file: tests/cookie.key # TODO: store and read as pem files, or read bytes directly from file? memc_url: memcache://memcache:11211 metrics_addr: server metrics_port: 8000 upstream_host: localhost upstream_port: 456 ================================================ FILE: tests/ntp-upstream-config.yaml ================================================ addr: - 127.0.0.1:456 cookie_key_file: tests/cookie.key # TODO: store and read as pem files, or read bytes directly from file? memc_url: memcache://memcache:11211 metrics_addr: server metrics_port: 8002 ================================================ FILE: tests/nts-ke-config.yaml ================================================ addr: - "[::]:4460" tls_key_file: tests/tls-pkcs8.pem tls_cert_file: tests/chain.pem # Expect PEM. cookie_key_file: tests/cookie.key # TODO: store and read as pem files, or read bytes directly from file? memc_url: memcache://memcache:11211 next_port: 123 metrics_addr: server metrics_port: 8001 ================================================ FILE: tests/test-config.json ================================================ { "signing": { "default": { "expiry": "876000h", "usages": [ "digital signature", "cert sign", "crl sign", "signing" ] } } } ================================================ FILE: tests/test.json ================================================ { "CN": "localhost", "hosts": [ "server", "localhost", "bogus.com", "*.localhost" ], "key": { "algo": "ecdsa", "size": 256 }, "names": [ { "C": "US", "ST": "CA", "L": "San Francisco", "O": "Cloudflare test", "OU": "Crypto team" } ] } ================================================ FILE: tests/tls-key.pem ================================================ -----BEGIN EC PRIVATE KEY----- MHcCAQEEIBvLtCg67XQkDzWZDS4peNXy8r4Dguv+KYUVoOZEcCjGoAoGCCqGSM49 AwEHoUQDQgAEcbTCrE0KE16aQz8DDTlEEHS+z1p3ILW0ib6mhuoHRPpPnQc3n014 PEyx94AtYE3+LbcUqmHB00YjM7c2Q2EyDQ== -----END EC PRIVATE KEY----- ================================================ FILE: tests/tls-pkcs8.pem ================================================ -----BEGIN PRIVATE KEY----- MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgG8u0KDrtdCQPNZkN Lil41fLyvgOC6/4phRWg5kRwKMahRANCAARxtMKsTQoTXppDPwMNOUQQdL7PWncg tbSJvqaG6gdE+k+dBzefTXg8TLH3gC1gTf4ttxSqYcHTRiMztzZDYTIN -----END PRIVATE KEY----- ================================================ FILE: tests/tls.csr ================================================ -----BEGIN CERTIFICATE REQUEST----- MIIBeDCCAR8CAQAwdjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQH Ew1TYW4gRnJhbmNpc2NvMRgwFgYDVQQKEw9DbG91ZGZsYXJlIHRlc3QxFDASBgNV BAsTC0NyeXB0byB0ZWFtMRIwEAYDVQQDEwlsb2NhbGhvc3QwWTATBgcqhkjOPQIB BggqhkjOPQMBBwNCAARxtMKsTQoTXppDPwMNOUQQdL7PWncgtbSJvqaG6gdE+k+d BzefTXg8TLH3gC1gTf4ttxSqYcHTRiMztzZDYTINoEcwRQYJKoZIhvcNAQkOMTgw NjA0BgNVHREELTArggZzZXJ2ZXKCCWxvY2FsaG9zdIIJYm9ndXMuY29tggsqLmxv Y2FsaG9zdDAKBggqhkjOPQQDAgNHADBEAiBGUmLvyO5eqzQIsEB2v4ysI8vDLrDV lRSABgL6YpJPOwIgSeSy73gwaBWRk/EVlahptbUSGcNPYa3m2rlAtTKX2Vo= -----END CERTIFICATE REQUEST----- ================================================ FILE: tests/tls.pem ================================================ -----BEGIN CERTIFICATE----- MIICoDCCAkegAwIBAgIUW5W4GNGYJwryph3KHKkdLaeFdvMwCgYIKoZIzj0EAwIw gY4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T YW4gRnJhbmNpc2NvMRgwFgYDVQQKEw9IYXBweUNlcnQsIEluYy4xHzAdBgNVBAsT FkhhcHB5Q2VydCBJbnRlcm1lZGlhdGUxFzAVBgNVBAMTDihkZXYgdXNlIG9ubHkp MCAXDTI0MDQwOTA3MjEwMFoYDzIxMjQwMzE2MDcyMTAwWjB2MQswCQYDVQQGEwJV UzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xGDAWBgNVBAoT D0Nsb3VkZmxhcmUgdGVzdDEUMBIGA1UECxMLQ3J5cHRvIHRlYW0xEjAQBgNVBAMT CWxvY2FsaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHG0wqxNChNemkM/ Aw05RBB0vs9adyC1tIm+pobqB0T6T50HN59NeDxMsfeALWBN/i23FKphwdNGIzO3 NkNhMg2jgZcwgZQwDgYDVR0PAQH/BAQDAgGGMAwGA1UdEwEB/wQCMAAwHQYDVR0O BBYEFJw0MuxNY1BtEB3oNMbPiwxDNrqZMB8GA1UdIwQYMBaAFOu3ANbLR8TJecGR AAuxL7juFmVyMDQGA1UdEQQtMCuCBnNlcnZlcoIJbG9jYWxob3N0gglib2d1cy5j b22CCyoubG9jYWxob3N0MAoGCCqGSM49BAMCA0cAMEQCIAJ0Os/bYUfH6nPO8f1E vVateUJXKaPuS6jD3i0eWQYbAiAyC4TPPr4S0wUXGf6RYwbTPG3sAvGAnuxpxlqB P0sZrQ== -----END CERTIFICATE-----