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