Skip to content

Commit

Permalink
add custom rustls session ticketer
Browse files Browse the repository at this point in the history
  • Loading branch information
zh-jq-b committed Oct 20, 2023
1 parent 603b777 commit 8c545b3
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 19 deletions.
38 changes: 30 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions g3tiles/src/config/server/rustls_proxy/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ use std::time::Duration;

use anyhow::{anyhow, Context};
use rustls::server::AllowAnyAuthenticatedClient;
use rustls::{Certificate, RootCertStore, ServerConfig, Ticketer};
use rustls::{Certificate, RootCertStore, ServerConfig};
use yaml_rust::Yaml;

use g3_types::collection::NamedValue;
use g3_types::limit::RateLimitQuotaConfig;
use g3_types::net::{
MultipleCertResolver, RustlsCertificatePair, RustlsServerSessionCache, TcpSockSpeedLimitConfig,
MultipleCertResolver, RustlsCertificatePair, RustlsServerSessionCache, RustlsSessionTicketer,
TcpSockSpeedLimitConfig,
};
use g3_types::route::AlpnMatch;
use g3_yaml::{YamlDocPosition, YamlMapCallback};
Expand Down Expand Up @@ -113,8 +114,9 @@ impl RustlsHostConfig {

config.session_storage = Arc::new(RustlsServerSessionCache::default());
if self.use_session_ticket {
config.ticketer =
Ticketer::new().map_err(|e| anyhow!("failed to create ticketer: {e}"))?;
let ticketer =
RustlsSessionTicketer::new().context("failed to create session ticketer")?;
config.ticketer = Arc::new(ticketer);
}

if !self.services.is_empty() {
Expand Down
4 changes: 3 additions & 1 deletion lib/g3-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ once_cell = { workspace = true, optional = true }
regex = { workspace = true, optional = true }
radix_trie = { workspace = true, optional = true }
rustls = { workspace = true, optional = true }
ring = { version = "0.17", optional = true }
webpki-roots = { version = "0.25", optional = true }
rustls-pemfile = { workspace = true, optional = true }
rustls-native-certs = { workspace = true, optional = true }
Expand All @@ -46,12 +47,13 @@ flume = { workspace = true, features = ["eventual-fairness"], optional = true }
slog = { workspace = true, optional = true }
indexmap = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
arc-swap = { workspace = true, optional = true }

[features]
default = []
auth-crypt = ["dep:digest", "dep:md-5", "dep:sha-1", "dep:blake3", "dep:hex"]
resolve = ["dep:ahash", "dep:radix_trie"]
rustls = ["dep:rustls", "dep:webpki-roots", "dep:rustls-pemfile", "dep:rustls-native-certs", "dep:ahash", "dep:lru"]
rustls = ["dep:rustls", "dep:webpki-roots", "dep:rustls-pemfile", "dep:rustls-native-certs", "dep:ahash", "dep:lru", "dep:ring", "dep:arc-swap"]
openssl = ["dep:openssl", "dep:ahash", "dep:lru"]
vendored-tongsuo = ["openssl", "openssl/tongsuo"]
acl-rule = ["resolve", "dep:ahash", "dep:ip_network", "dep:ip_network_table", "dep:once_cell", "dep:regex", "dep:radix_trie", "proxy"]
Expand Down
File renamed without changes.
7 changes: 5 additions & 2 deletions lib/g3-types/src/net/rustls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ pub use client::{RustlsClientConfig, RustlsClientConfigBuilder};
mod server;
pub use server::{RustlsServerConfig, RustlsServerConfigBuilder};

mod session;
pub use session::RustlsServerSessionCache;
mod cache;
pub use cache::RustlsServerSessionCache;

mod ticket;
pub use ticket::RustlsSessionTicketer;

mod cert_pair;
pub use cert_pair::RustlsCertificatePair;
Expand Down
11 changes: 7 additions & 4 deletions lib/g3-types/src/net/rustls/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ use std::time::Duration;

use anyhow::{anyhow, Context};
use rustls::server::AllowAnyAuthenticatedClient;
use rustls::{Certificate, RootCertStore, ServerConfig, Ticketer};
use rustls::{Certificate, RootCertStore, ServerConfig};

use super::{MultipleCertResolver, RustlsCertificatePair, RustlsServerSessionCache};
use super::{
MultipleCertResolver, RustlsCertificatePair, RustlsServerSessionCache, RustlsSessionTicketer,
};
use crate::net::tls::AlpnProtocol;

#[derive(Clone)]
Expand Down Expand Up @@ -127,8 +129,9 @@ impl RustlsServerConfigBuilder {
};
config.session_storage = Arc::new(RustlsServerSessionCache::default());
if self.use_session_ticket {
config.ticketer =
Ticketer::new().map_err(|e| anyhow!("failed to create ticketer: {e}"))?;
let ticketer =
RustlsSessionTicketer::new().context("failed to create session ticketer")?;
config.ticketer = Arc::new(ticketer);
}

if let Some(protocols) = alpn_protocols {
Expand Down
180 changes: 180 additions & 0 deletions lib/g3-types/src/net/rustls/ticket.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright 2023 ByteDance and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use std::sync::{Arc, Mutex};
use std::time;

use anyhow::anyhow;
use arc_swap::ArcSwap;
use rand::Fill;
use ring::aead;
use rustls::server::ProducesTickets;

#[derive(Clone)]
struct AeadKey(aead::LessSafeKey);

impl AeadKey {
/// Make a ticketer with recommended configuration and a random key.
fn new() -> Result<Self, rand::Error> {
let mut key = [0u8; 32];
key.try_fill(&mut rand::thread_rng())?;

let alg = &aead::CHACHA20_POLY1305;
let key = aead::UnboundKey::new(alg, &key).unwrap();

Ok(Self(aead::LessSafeKey::new(key)))
}

/// Encrypt `message` and return the ciphertext.
fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
// Random nonce, because a counter is a privacy leak.
let mut nonce_buf = [0u8; 12];
nonce_buf.try_fill(&mut rand::thread_rng()).ok()?;
let nonce = aead::Nonce::assume_unique_for_key(nonce_buf);
let aad = aead::Aad::empty();

let mut ciphertext =
Vec::with_capacity(nonce_buf.len() + message.len() + self.0.algorithm().tag_len());
ciphertext.extend(nonce_buf);
ciphertext.extend(message);
self.0
.seal_in_place_separate_tag(nonce, aad, &mut ciphertext[nonce_buf.len()..])
.map(|tag| {
ciphertext.extend(tag.as_ref());
ciphertext
})
.ok()
}

/// Decrypt `ciphertext` and recover the original message.
fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
// Non-panicking `let (nonce, ciphertext) = ciphertext.split_at(...)`.
let nonce = ciphertext.get(..self.0.algorithm().nonce_len())?;
let ciphertext = ciphertext.get(nonce.len()..)?;

// This won't fail since `nonce` has the required length.
let nonce = aead::Nonce::try_assume_unique_for_key(nonce).ok()?;

let mut out = Vec::from(ciphertext);

let plain_len = self
.0
.open_in_place(nonce, aead::Aad::empty(), &mut out)
.ok()?
.len();
out.truncate(plain_len);

Some(out)
}
}

struct AeadKeys {
current: AeadKey,
previous: Option<AeadKey>,
expire_time: u64,
}

impl AeadKeys {
fn new(expire_time: u64) -> Result<Self, rand::Error> {
let current = AeadKey::new()?;
Ok(AeadKeys {
current,
previous: None,
expire_time,
})
}

fn rotate_new(&self, expire_time: u64) -> Option<Self> {
let current = AeadKey::new().ok()?;
let previous = self.current.clone();
Some(AeadKeys {
current,
previous: Some(previous),
expire_time,
})
}
}

pub struct RustlsSessionTicketer {
lifetime: u32,
keys: ArcSwap<AeadKeys>,
lock: Mutex<Arc<AeadKeys>>,
}

impl RustlsSessionTicketer {
pub fn new() -> Result<Self, anyhow::Error> {
let time_now = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.map_err(|e| anyhow!("failed to get timestamp now: {e}"))?
.as_secs();
let lifetime: u32 = 6 * 60 * 60;
let keys = AeadKeys::new(time_now.saturating_add(lifetime as u64))
.map_err(|e| anyhow!("failed to create aead keys: {e}"))?;
let keys = Arc::new(keys);
Ok(RustlsSessionTicketer {
lifetime,
keys: ArcSwap::new(keys.clone()),
lock: Mutex::new(keys),
})
}
}

impl ProducesTickets for RustlsSessionTicketer {
fn enabled(&self) -> bool {
true
}

fn lifetime(&self) -> u32 {
self.lifetime
}

fn encrypt(&self, plain: &[u8]) -> Option<Vec<u8>> {
let keys = self.keys.load_full();

let time_now = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.ok()?
.as_secs();
if time_now < keys.expire_time {
return keys.current.encrypt(plain);
}

let mut locked_keys = self.lock.lock().unwrap();
if time_now < locked_keys.expire_time {
let keys = locked_keys.clone();
drop(locked_keys);

// no need to keep a full reference as we have just switched
keys.current.encrypt(plain)
} else {
let new_keys = locked_keys.rotate_new(time_now.saturating_add(self.lifetime as u64))?;
let new_keys = Arc::new(new_keys);
self.keys.store(new_keys.clone());
*locked_keys = new_keys.clone();
drop(locked_keys);

new_keys.current.encrypt(plain)
}
}

fn decrypt(&self, cipher: &[u8]) -> Option<Vec<u8>> {
let keys = self.keys.load_full();
// Decrypt with the current key; if that fails, try with the previous.
keys.current
.decrypt(cipher)
.or_else(|| keys.previous.as_ref().and_then(|p| p.decrypt(cipher)))
}
}

0 comments on commit 8c545b3

Please sign in to comment.