temporarily implemented client authentication using client certificates (mTLS)
This commit is contained in:
parent
8f7f9d4257
commit
d7193af4e6
21 changed files with 326 additions and 40 deletions
11
Cargo.toml
11
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "rpxy"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
authors = ["Jun Kurihara"]
|
||||
homepage = "https://github.com/junkurihara/rust-rpxy"
|
||||
repository = "https://github.com/junkurihara/rust-rpxy"
|
||||
|
|
@ -18,7 +18,7 @@ http3 = ["quinn", "h3", "h3-quinn"]
|
|||
[dependencies]
|
||||
env_logger = "0.9.1"
|
||||
anyhow = "1.0.65"
|
||||
clap = { version = "3.2.22", features = ["std", "cargo", "wrap_help"] }
|
||||
clap = { version = "4.0.4", features = ["std", "cargo", "wrap_help"] }
|
||||
futures = { version = "0.3.24", features = ["alloc", "async-await"] }
|
||||
hyper = { version = "0.14.20", default-features = false, features = [
|
||||
"server",
|
||||
|
|
@ -27,7 +27,7 @@ hyper = { version = "0.14.20", default-features = false, features = [
|
|||
"stream",
|
||||
] }
|
||||
log = "0.4.17"
|
||||
tokio = { version = "1.21.1", default-features = false, features = [
|
||||
tokio = { version = "1.21.2", default-features = false, features = [
|
||||
"net",
|
||||
"rt-multi-thread",
|
||||
"parking_lot",
|
||||
|
|
@ -41,7 +41,7 @@ rustls = { version = "0.20.6", default-features = false }
|
|||
rand = "0.8.5"
|
||||
toml = { version = "0.5.9", default-features = false }
|
||||
rustc-hash = "1.1.0"
|
||||
serde = { version = "1.0.144", default-features = false, features = ["derive"] }
|
||||
serde = { version = "1.0.145", default-features = false, features = ["derive"] }
|
||||
hyper-rustls = { version = "0.23.0", default-features = false, features = [
|
||||
"tokio-runtime",
|
||||
"webpki-tokio",
|
||||
|
|
@ -52,7 +52,8 @@ bytes = "1.2.1"
|
|||
quinn = { version = "0.8.5", optional = true }
|
||||
h3 = { path = "./h3/h3/", optional = true }
|
||||
h3-quinn = { path = "./h3/h3-quinn/", optional = true }
|
||||
thiserror = "1.0.35"
|
||||
thiserror = "1.0.37"
|
||||
x509-parser = "0.14.0"
|
||||
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
|
|
|
|||
58
README.md
58
README.md
|
|
@ -128,7 +128,7 @@ listen_port_tls = 443
|
|||
|
||||
[apps."app_name"]
|
||||
server_name = 'app1.example.com'
|
||||
tls = { tls_cert_path = 'localhost.crt', tls_cert_key_path = 'localhost.key' }
|
||||
tls = { tls_cert_path = 'server.crt', tls_cert_key_path = 'server.key' }
|
||||
reverse_proxy = [{ upstream = [{ location = 'app1.local:8080' }] }]
|
||||
```
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ We should note that the private key specified by `tls_cert_key_path` must be *in
|
|||
In the current Web, we believe it is common to serve everything through HTTPS rather than HTTP, and hence *https redirection* is often used for HTTP requests. When you specify both `listen_port` and `listen_port_tls`, you can enable an option of such redirection by making `https_redirection` true.
|
||||
|
||||
```toml
|
||||
tls = { https_redirection = true, tls_cert_path = 'localhost.crt', tls_cert_key_path = 'localhost.key' }
|
||||
tls = { https_redirection = true, tls_cert_path = 'server.crt', tls_cert_key_path = 'server.key' }
|
||||
```
|
||||
|
||||
If it is true, `rpxy` returns the status code `301` to the cleartext request with new location `https://<requested_host>/<requested_query_and_path>` served over TLS.
|
||||
|
|
@ -155,7 +155,7 @@ listen_port_tls = 443
|
|||
|
||||
[apps.app1]
|
||||
server_name = 'app1.example.com'
|
||||
tls = { https_redirection = true, tls_cert_path = 'localhost.crt', tls_cert_key_path = 'localhost.key' }
|
||||
tls = { https_redirection = true, tls_cert_path = 'server.crt', tls_cert_key_path = 'server.key' }
|
||||
|
||||
[[apps.app1.reverse_proxy]]
|
||||
upstream = [
|
||||
|
|
@ -235,13 +235,63 @@ If you obtain certificates and private keys from [Let's Encrypt](https://letsenc
|
|||
The easiest way is to use `openssl` by
|
||||
|
||||
```bash
|
||||
openssl pkcs8 -topk8 -nocrypt \
|
||||
$ openssl pkcs8 -topk8 -nocrypt \
|
||||
-in yoru_domain_from_le.key \
|
||||
-inform PEM \
|
||||
-out your_domain_pkcs8.key.pem \
|
||||
-outform PEM
|
||||
```
|
||||
|
||||
### Client Authentication using Client Certificate Signed by Your Own Root CA
|
||||
|
||||
First, you need to prepare a CA certificate used to verify client certificate. If you do not have one, you can generate CA key and certificate by OpenSSL commands as follows. *Note that `rustls` accepts X509v3 certificates and reject SHA-1, and that `rpxy` relys on Version 3 extension fields of `KeyID`s of `Subject Key Identifier` and `Authority Key Identifier`.*
|
||||
|
||||
1. Generate CA key of `secp256v1`, CSR, and then generate CA certificate that will be set for `tls.client_ca_cert_path` for each server app in `config.toml`.
|
||||
|
||||
```bash
|
||||
$ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out client.ca.key
|
||||
|
||||
$ openssl req -new -key client.ca.key -out client.ca.csr
|
||||
...
|
||||
-----
|
||||
Country Name (2 letter code) []: ...
|
||||
State or Province Name (full name) []: ...
|
||||
Locality Name (eg, city) []: ...
|
||||
Organization Name (eg, company) []: ...
|
||||
Organizational Unit Name (eg, section) []: ...
|
||||
Common Name (eg, fully qualified host name) []: <Should not input CN>
|
||||
Email Address []: ...
|
||||
|
||||
$ openssl x509 -req -days 3650 -sha256 -in client.ca.csr -signkey client.ca.key -out client.ca.crt -extfile client.ca.ext
|
||||
```
|
||||
|
||||
2. Generate a client key of `secp256v1` and certificate signed by CA key.
|
||||
|
||||
```bash
|
||||
$ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out client.key
|
||||
|
||||
$ openssl req -new -key client.key -out client.csr
|
||||
...
|
||||
-----
|
||||
Country Name (2 letter code) []:
|
||||
State or Province Name (full name) []:
|
||||
Locality Name (eg, city) []:
|
||||
Organization Name (eg, company) []:
|
||||
Organizational Unit Name (eg, section) []:
|
||||
Common Name (eg, fully qualified host name) []: <Should not input CN>
|
||||
Email Address []:
|
||||
|
||||
$ openssl x509 -req -days 365 -sha256 -in client.csr -CA client.ca.crt -CAkey client.ca.key -CAcreateserial -out client.crt -extfile client.ext
|
||||
```
|
||||
|
||||
Now you have a client key `client.key` and certificate `client.crt` (version 3). `p12` file can be retrieved as
|
||||
|
||||
```bash
|
||||
$ openssl pkcs12 -export -inkey client.key -in client.crt -certfile client.ca.crt -out client.pfx
|
||||
```
|
||||
|
||||
All of sample certificate files are found in `./example-certs/` directory.
|
||||
|
||||
### (Work Around) Deployment on Ubuntu 22.04LTS using docker behind `ufw`
|
||||
|
||||
Basically, docker automatically manage your iptables if you use the port-mapping option, i.e., `--publish` for `docker run` or `ports` in `docker-compose.yml`. This means you do not need to manually expose your port, e.g., 443 TCP/UDP for HTTPS, using `ufw`-like management command.
|
||||
|
|
|
|||
2
TODO.md
2
TODO.md
|
|
@ -7,5 +7,5 @@
|
|||
- Options to serve custom http_error page.
|
||||
- Prometheus metrics
|
||||
- Documentation
|
||||
- Client certificate
|
||||
- Client certificate -> support intermediate certificate. Currently, only supports client certificates directly signed by root CA.
|
||||
- etc.
|
||||
|
|
|
|||
|
|
@ -37,8 +37,9 @@ default_app = 'another_localhost'
|
|||
server_name = 'localhost' # Domain name
|
||||
|
||||
# Optional: TLS setting. if https_port is specified and tls is true above, this must be given.
|
||||
tls = { https_redirection = true, tls_cert_path = '/certs/localhost.crt', tls_cert_key_path = '/certs/localhost.key' } # for docker volume mounted certs
|
||||
#tls = { https_redirection = true, tls_cert_path = './localhost.crt', tls_cert_key_path = './localhost.key' } # for local
|
||||
tls = { https_redirection = true, tls_cert_path = '/certs/server.crt', tls_cert_key_path = '/certs/server.key' } # for docker volume mounted certs
|
||||
#tls = { https_redirection = true, tls_cert_path = './server.crt', tls_cert_key_path = './server.key' } # for local
|
||||
#tls = { https_redirection = true, tls_cert_path = './server.crt', tls_cert_key_path = './server.key', client_ca_cert_path = './client_cert.ca.crt' } # for local with client_cert
|
||||
|
||||
## TODO
|
||||
# allowhosts = ['127.0.0.1', '::1', '192.168.10.0/24'] # TODO
|
||||
|
|
|
|||
12
example-certs/client.ca.crt
Normal file
12
example-certs/client.ca.crt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIB0DCCAXegAwIBAgIUeW2Vdqq6y9H0TFvClTW9YkwNlcMwCgYIKoZIzj0EAwIw
|
||||
PjELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMQ0wCwYDVQQHDARDaHVvMRAw
|
||||
DgYDVQQKDAdaZXR0YW50MB4XDTIyMTAwMzE0MTAxM1oXDTMyMDkzMDE0MTAxM1ow
|
||||
PjELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMQ0wCwYDVQQHDARDaHVvMRAw
|
||||
DgYDVQQKDAdaZXR0YW50MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuLuDStaw
|
||||
D2jTIDUUFDnT2X01kNPAv2UK5QAzwjQPqu61koNHsRSe1GuhkC2jFolCaapOTWnJ
|
||||
E7EeesOXtihI4KNTMFEwHQYDVR0OBBYEFBHfSGDdI6/YEwGrfPUuFAFxO5ejMB8G
|
||||
A1UdIwQYMBaAFBHfSGDdI6/YEwGrfPUuFAFxO5ejMA8GA1UdEwEB/wQFMAMBAf8w
|
||||
CgYIKoZIzj0EAwIDRwAwRAIgBfmM5qivBXQbLOH9+XI4D8ah0nrNjZvTYMS0V32d
|
||||
888CIF33NCYYf+LB/edqkQeyU/Xuw4pOx72MD3GPJG1lYWkW
|
||||
-----END CERTIFICATE-----
|
||||
5
example-certs/client.ca.key
Normal file
5
example-certs/client.ca.key
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgj6mxDmE5gPgJ5yQY
|
||||
pJByP2UL67EwcBHJEVed77CHmRuhRANCAAS4u4NK1rAPaNMgNRQUOdPZfTWQ08C/
|
||||
ZQrlADPCNA+q7rWSg0exFJ7Ua6GQLaMWiUJpqk5NackTsR56w5e2KEjg
|
||||
-----END PRIVATE KEY-----
|
||||
13
example-certs/client.crt
Normal file
13
example-certs/client.crt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIB3jCCAYSgAwIBAgIUJg74LEgATwFv6xAvbcILjHAx2k4wCgYIKoZIzj0EAwIw
|
||||
PjELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMQ0wCwYDVQQHDARDaHVvMRAw
|
||||
DgYDVQQKDAdaZXR0YW50MB4XDTIyMTAwMzE0MTEwM1oXDTIzMTAwMjE0MTEwM1ow
|
||||
RDELMAkGA1UEBhMCSlAxDjAMBgNVBAgMBVRva3lvMQ8wDQYDVQQHDAZOZXJpbWEx
|
||||
FDASBgNVBAoMC1pldHRhbnQgRGV2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
|
||||
C8CxHow7SH0ZuRzFmquRl8lvwlwAuARZBPnT3u44BPwqDerI97555JkKquk35F6g
|
||||
mFLPB28ljIvMicBDqjzS56NaMFgwHwYDVR0jBBgwFoAUEd9IYN0jr9gTAat89S4U
|
||||
AXE7l6MwCQYDVR0TBAIwADALBgNVHQ8EBAMCBPAwHQYDVR0OBBYEFBuXDF84bxZt
|
||||
qK7Y/ngSgm5JHgC2MAoGCCqGSM49BAMCA0gAMEUCIQD2yl6pYXuPnOSne4+yHOw3
|
||||
PdhPlyARxQqhrWM2LITP4AIgMv+exuURpaVj4ykhmlGS7ut05qZBpVgH4E+gamn2
|
||||
ZW8=
|
||||
-----END CERTIFICATE-----
|
||||
8
example-certs/client.csr
Normal file
8
example-certs/client.csr
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBADCBpgIBADBEMQswCQYDVQQGEwJKUDEOMAwGA1UECAwFVG9reW8xDzANBgNV
|
||||
BAcMBk5lcmltYTEUMBIGA1UECgwLWmV0dGFudCBEZXYwWTATBgcqhkjOPQIBBggq
|
||||
hkjOPQMBBwNCAAQLwLEejDtIfRm5HMWaq5GXyW/CXAC4BFkE+dPe7jgE/CoN6sj3
|
||||
vnnkmQqq6TfkXqCYUs8HbyWMi8yJwEOqPNLnoAAwCgYIKoZIzj0EAwIDSQAwRgIh
|
||||
AJ0KUTO7x6YvavdLHllW9HWiSyeztquAQrqqHzO7sAHmAiEAitDM1Jv3xHbeK83R
|
||||
ihWMGj/8y+QMeaL7cPBY/dfwIis=
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
4
example-certs/client.ext
Normal file
4
example-certs/client.ext
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
authorityKeyIdentifier=keyid:always
|
||||
basicConstraints=CA:FALSE
|
||||
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
|
||||
|
||||
5
example-certs/client.key
Normal file
5
example-certs/client.key
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgQ70UolUBJK41lMWU
|
||||
4fid9INB08+kWF5NgXmLr3VknvahRANCAAQLwLEejDtIfRm5HMWaq5GXyW/CXAC4
|
||||
BFkE+dPe7jgE/CoN6sj3vnnkmQqq6TfkXqCYUs8HbyWMi8yJwEOqPNLn
|
||||
-----END PRIVATE KEY-----
|
||||
2
h3
2
h3
|
|
@ -1 +1 @@
|
|||
Subproject commit 27ef38d3940eb35205a6d8b1dd7c3f8d278d77b4
|
||||
Subproject commit 92b67269c2a24c0beeac0bbdd0c886dd886c3ad6
|
||||
|
|
@ -5,7 +5,8 @@ use crate::{
|
|||
log::*,
|
||||
utils::{BytesName, PathNameBytesExp, ServerNameBytesExp},
|
||||
};
|
||||
use rustc_hash::FxHashMap as HashMap;
|
||||
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
||||
use rustls::OwnedTrustAnchor;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufReader, Cursor, Read},
|
||||
|
|
@ -19,6 +20,7 @@ use tokio_rustls::rustls::{
|
|||
};
|
||||
pub use upstream::{ReverseProxy, Upstream, UpstreamGroup};
|
||||
pub use upstream_opts::UpstreamOption;
|
||||
use x509_parser::prelude::*;
|
||||
|
||||
/// Struct serving information to route incoming connections, like server name to be handled and tls certs/keys settings.
|
||||
pub struct Backend {
|
||||
|
|
@ -30,6 +32,7 @@ pub struct Backend {
|
|||
pub tls_cert_path: Option<PathBuf>,
|
||||
pub tls_cert_key_path: Option<PathBuf>,
|
||||
pub https_redirection: Option<bool>,
|
||||
pub client_ca_cert_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
|
|
@ -105,6 +108,66 @@ impl Backend {
|
|||
})?;
|
||||
Ok(CertifiedKey::new(certs, signing_key))
|
||||
}
|
||||
|
||||
fn read_client_ca_certs(&self) -> io::Result<(Vec<OwnedTrustAnchor>, HashSet<Vec<u8>>)> {
|
||||
debug!("Read CA certificate for client authentication");
|
||||
// Reads client certificate and returns client
|
||||
let client_ca_cert_path = {
|
||||
if let Some(c) = self.client_ca_cert_path.as_ref() {
|
||||
c
|
||||
} else {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Invalid certs and keys paths"));
|
||||
}
|
||||
};
|
||||
let certs: Vec<_> = {
|
||||
let certs_path_str = client_ca_cert_path.display().to_string();
|
||||
let mut reader = BufReader::new(File::open(client_ca_cert_path).map_err(|e| {
|
||||
io::Error::new(
|
||||
e.kind(),
|
||||
format!("Unable to load the client certificates [{}]: {}", certs_path_str, e),
|
||||
)
|
||||
})?);
|
||||
rustls_pemfile::certs(&mut reader)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Unable to parse the client certificates"))?
|
||||
}
|
||||
.drain(..)
|
||||
.map(Certificate)
|
||||
.collect();
|
||||
|
||||
let owned_trust_anchors: Vec<_> = certs
|
||||
.iter()
|
||||
.map(|v| {
|
||||
let trust_anchor = tokio_rustls::webpki::TrustAnchor::try_from_cert_der(&v.0).unwrap();
|
||||
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
|
||||
trust_anchor.subject,
|
||||
trust_anchor.spki,
|
||||
trust_anchor.name_constraints,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let subject_key_identifiers: HashSet<_> = certs
|
||||
.iter()
|
||||
.filter_map(|v| {
|
||||
// retrieve ca key id (subject key id)
|
||||
let cert = parse_x509_certificate(&v.0).unwrap().1;
|
||||
let subject_key_ids = cert
|
||||
.iter_extensions()
|
||||
.filter_map(|ext| match ext.parsed_extension() {
|
||||
ParsedExtension::SubjectKeyIdentifier(skid) => Some(skid),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !subject_key_ids.is_empty() {
|
||||
Some(subject_key_ids[0].0.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok((owned_trust_anchors, subject_key_identifiers))
|
||||
}
|
||||
}
|
||||
|
||||
/// HashMap and some meta information for multiple Backend structs.
|
||||
|
|
@ -113,12 +176,20 @@ pub struct Backends {
|
|||
pub default_server_name_bytes: Option<ServerNameBytesExp>, // for plaintext http
|
||||
}
|
||||
|
||||
pub type SniKeyIdsMap = HashMap<ServerNameBytesExp, HashSet<Vec<u8>>>;
|
||||
pub struct ServerCrypto {
|
||||
pub inner: Arc<ServerConfig>,
|
||||
pub server_name_client_ca_keyids_map: Arc<SniKeyIdsMap>,
|
||||
}
|
||||
|
||||
impl Backends {
|
||||
pub async fn generate_server_crypto_with_cert_resolver(&self) -> Result<ServerConfig, anyhow::Error> {
|
||||
pub async fn generate_server_crypto_with_cert_resolver(&self) -> Result<ServerCrypto, anyhow::Error> {
|
||||
let mut resolver = ResolvesServerCertUsingSni::new();
|
||||
let mut client_ca_roots = rustls::RootCertStore::empty();
|
||||
let mut client_ca_key_ids: SniKeyIdsMap = HashMap::default();
|
||||
|
||||
// let mut cnt = 0;
|
||||
for (_, backend) in self.apps.iter() {
|
||||
for (server_name_bytes_exp, backend) in self.apps.iter() {
|
||||
if backend.tls_cert_key_path.is_some() && backend.tls_cert_path.is_some() {
|
||||
match backend.read_certs_and_key() {
|
||||
Ok(certified_key) => {
|
||||
|
|
@ -137,14 +208,40 @@ impl Backends {
|
|||
warn!("Failed to add certificate for {}: {}", backend.server_name.as_str(), e);
|
||||
}
|
||||
}
|
||||
// add client certificate if specified
|
||||
if backend.client_ca_cert_path.is_some() {
|
||||
match backend.read_client_ca_certs() {
|
||||
Ok((owned_trust_anchors, subject_key_ids)) => {
|
||||
// TODO: ここでSubject Key ID (CA Key ID)を記録しておく。認証後にpeer certificateのauthority key idとの一貫性をチェック。
|
||||
// v3 x509前提で特定のkey id extが入ってなければ使えない前提
|
||||
client_ca_roots.add_server_trust_anchors(owned_trust_anchors.into_iter());
|
||||
client_ca_key_ids.insert(server_name_bytes_exp.to_owned(), subject_key_ids);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to add client ca certificate for {}: {}",
|
||||
backend.server_name.as_str(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// debug!("Load certificate chain for {} server_name's", cnt);
|
||||
|
||||
//////////////
|
||||
// TODO: Client Certs
|
||||
let client_certs_verifier = rustls::server::AllowAnyAnonymousOrAuthenticatedClient::new(client_ca_roots);
|
||||
// No ClientCert or WithClientCert
|
||||
// let client_certs_verifier = rustls::server::AllowAnyAuthenticatedClient::new(client_ca_roots);
|
||||
|
||||
let mut server_config = ServerConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_no_client_auth()
|
||||
// .with_no_client_auth()
|
||||
.with_client_cert_verifier(client_certs_verifier)
|
||||
.with_cert_resolver(Arc::new(resolver));
|
||||
//////////////////////////////
|
||||
|
||||
#[cfg(feature = "http3")]
|
||||
{
|
||||
|
|
@ -160,6 +257,9 @@ impl Backends {
|
|||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
||||
}
|
||||
|
||||
Ok(server_config)
|
||||
Ok(ServerCrypto {
|
||||
inner: Arc::new(server_config),
|
||||
server_name_client_ca_keyids_map: Arc::new(client_ca_key_ids),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ pub fn parse_opts(globals: &mut Globals) -> std::result::Result<(), anyhow::Erro
|
|||
Arg::new("config_file")
|
||||
.long("config")
|
||||
.short('c')
|
||||
.takes_value(true)
|
||||
.value_name("FILE")
|
||||
.help("Configuration file path like \"./config.toml\""),
|
||||
);
|
||||
let matches = options.get_matches();
|
||||
|
||||
let config = if let Some(config_file_path) = matches.value_of("config_file") {
|
||||
let config = if let Some(config_file_path) = matches.get_one::<String>("config_file") {
|
||||
ConfigToml::new(config_file_path)?
|
||||
} else {
|
||||
// Default config Toml
|
||||
|
|
@ -93,9 +93,9 @@ pub fn parse_opts(globals: &mut Globals) -> std::result::Result<(), anyhow::Erro
|
|||
let server_name_string = app.server_name.as_ref().unwrap();
|
||||
|
||||
// TLS settings
|
||||
let (tls_cert_path, tls_cert_key_path, https_redirection) = if app.tls.is_none() {
|
||||
let (tls_cert_path, tls_cert_key_path, https_redirection, client_ca_cert_path) = if app.tls.is_none() {
|
||||
ensure!(globals.http_port.is_some(), "Required HTTP port");
|
||||
(None, None, None)
|
||||
(None, None, None, None)
|
||||
} else {
|
||||
let tls = app.tls.as_ref().unwrap();
|
||||
ensure!(tls.tls_cert_key_path.is_some() && tls.tls_cert_path.is_some());
|
||||
|
|
@ -109,6 +109,7 @@ pub fn parse_opts(globals: &mut Globals) -> std::result::Result<(), anyhow::Erro
|
|||
ensure!(globals.https_port.is_some()); // only when both https ports are configured.
|
||||
tls.https_redirection
|
||||
},
|
||||
tls.client_ca_cert_path.as_ref().map(PathBuf::from),
|
||||
)
|
||||
};
|
||||
if globals.http_port.is_none() {
|
||||
|
|
@ -130,6 +131,7 @@ pub fn parse_opts(globals: &mut Globals) -> std::result::Result<(), anyhow::Erro
|
|||
tls_cert_path,
|
||||
tls_cert_key_path,
|
||||
https_redirection,
|
||||
client_ca_cert_path,
|
||||
},
|
||||
);
|
||||
info!("Registering application: {} ({})", app_name, server_name_string);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ pub struct TlsOption {
|
|||
pub tls_cert_path: Option<String>,
|
||||
pub tls_cert_key_path: Option<String>,
|
||||
pub https_redirection: Option<bool>,
|
||||
pub client_ca_cert_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Default)]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
mod proxy_client_cert;
|
||||
#[cfg(feature = "http3")]
|
||||
mod proxy_h3;
|
||||
mod proxy_main;
|
||||
|
|
|
|||
45
src/proxy/proxy_client_cert.rs
Normal file
45
src/proxy/proxy_client_cert.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use crate::{error::*, log::*};
|
||||
use rustc_hash::FxHashSet as HashSet;
|
||||
use rustls::Certificate;
|
||||
use x509_parser::extensions::ParsedExtension;
|
||||
use x509_parser::prelude::*;
|
||||
|
||||
// TODO: consider move this function to the layer of handle_request (L7) to return 403
|
||||
pub(super) fn check_client_authentication(
|
||||
client_certs: Option<&[Certificate]>,
|
||||
client_certs_setting_for_sni: Option<&HashSet<Vec<u8>>>,
|
||||
) -> Result<()> {
|
||||
if let Some(client_ca_keyids_set) = client_certs_setting_for_sni {
|
||||
if let Some(client_certs) = client_certs {
|
||||
debug!("Incoming TLS client is (temporarily) authenticated via client cert");
|
||||
// Check client certificate key ids
|
||||
|
||||
let mut client_certs_parsed_iter = client_certs.iter().filter_map(|d| parse_x509_certificate(&d.0).ok());
|
||||
let match_server_crypto_and_client_cert = client_certs_parsed_iter.any(|c| {
|
||||
let mut filtered = c.1.iter_extensions().filter_map(|e| {
|
||||
if let ParsedExtension::AuthorityKeyIdentifier(key_id) = e.parsed_extension() {
|
||||
key_id.key_identifier.as_ref()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
filtered.any(|id| client_ca_keyids_set.contains(id.0))
|
||||
});
|
||||
if !match_server_crypto_and_client_cert {
|
||||
// TODO: return 403 here
|
||||
error!("Inconsistent client certificate for given server name");
|
||||
return Err(RpxyError::Proxy(
|
||||
"Inconsistent client certificate for given server name".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// TODO: return 403 here
|
||||
error!("Client certificate is needed for given server name");
|
||||
return Err(RpxyError::Proxy(
|
||||
"Client certificate is needed for given server name".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
use super::Proxy;
|
||||
use crate::{error::*, log::*, utils::ServerNameBytesExp};
|
||||
use super::{proxy_client_cert::check_client_authentication, Proxy};
|
||||
use crate::{backend::SniKeyIdsMap, error::*, log::*, utils::ServerNameBytesExp};
|
||||
use bytes::{Buf, Bytes};
|
||||
use h3::{quic::BidiStream, server::RequestStream};
|
||||
use hyper::{client::connect::Connect, Body, Request, Response};
|
||||
use std::net::SocketAddr;
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
impl<T> Proxy<T>
|
||||
|
|
@ -14,11 +14,28 @@ where
|
|||
self,
|
||||
conn: quinn::Connecting,
|
||||
tls_server_name: ServerNameBytesExp,
|
||||
sni_cc_map: Arc<SniKeyIdsMap>,
|
||||
) -> Result<()> {
|
||||
let client_addr = conn.remote_address();
|
||||
|
||||
match conn.await {
|
||||
Ok(new_conn) => {
|
||||
// Check client certificates
|
||||
// TODO: consider move this function to the layer of handle_request (L7) to return 403
|
||||
let cc = {
|
||||
// https://docs.rs/quinn/latest/quinn/struct.Connection.html
|
||||
let client_certs_setting_for_sni = sni_cc_map.get(&tls_server_name);
|
||||
let client_certs = match new_conn.connection.peer_identity() {
|
||||
Some(peer_identity) => peer_identity
|
||||
.downcast::<Vec<rustls::Certificate>>()
|
||||
.ok()
|
||||
.map(|p| p.into_iter().collect::<Vec<_>>()),
|
||||
None => None,
|
||||
};
|
||||
(client_certs, client_certs_setting_for_sni)
|
||||
};
|
||||
check_client_authentication(cc.0.as_ref().map(AsRef::as_ref), cc.1)?;
|
||||
|
||||
let mut h3_conn = h3::server::Connection::<_, bytes::Bytes>::new(h3_quinn::Connection::new(new_conn)).await?;
|
||||
info!(
|
||||
"QUIC/HTTP3 connection established from {:?} {:?}",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
use super::proxy_main::{LocalExecutor, Proxy};
|
||||
use crate::{constants::*, error::*, log::*, utils::BytesName};
|
||||
use super::{
|
||||
proxy_client_cert::check_client_authentication,
|
||||
proxy_main::{LocalExecutor, Proxy},
|
||||
};
|
||||
use crate::{
|
||||
backend::{ServerCrypto, SniKeyIdsMap},
|
||||
constants::*,
|
||||
error::*,
|
||||
log::*,
|
||||
utils::BytesName,
|
||||
};
|
||||
use hyper::{client::connect::Connect, server::conn::Http};
|
||||
use rustls::ServerConfig;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -19,7 +28,7 @@ impl<T> Proxy<T>
|
|||
where
|
||||
T: Connect + Clone + Sync + Send + 'static,
|
||||
{
|
||||
async fn cert_service(&self, server_crypto_tx: watch::Sender<Option<Arc<ServerConfig>>>) {
|
||||
async fn cert_service(&self, server_crypto_tx: watch::Sender<Option<Arc<ServerCrypto>>>) {
|
||||
info!("Start cert watch service");
|
||||
loop {
|
||||
if let Ok(server_crypto) = self.globals.backends.generate_server_crypto_with_cert_resolver().await {
|
||||
|
|
@ -38,21 +47,23 @@ where
|
|||
async fn listener_service(
|
||||
&self,
|
||||
server: Http<LocalExecutor>,
|
||||
mut server_crypto_rx: watch::Receiver<Option<Arc<ServerConfig>>>,
|
||||
mut server_crypto_rx: watch::Receiver<Option<Arc<ServerCrypto>>>,
|
||||
) -> Result<()> {
|
||||
let tcp_listener = TcpListener::bind(&self.listening_on).await?;
|
||||
info!("Start TCP proxy serving with HTTPS request for configured host names");
|
||||
|
||||
// let mut server_crypto: Option<Arc<ServerConfig>> = None;
|
||||
let mut tls_acceptor: Option<TlsAcceptor> = None;
|
||||
let mut sni_client_ca_keyid_map: Option<Arc<SniKeyIdsMap>> = None;
|
||||
loop {
|
||||
tokio::select! {
|
||||
tcp_cnx = tcp_listener.accept() => {
|
||||
if tls_acceptor.is_none() || tcp_cnx.is_err() {
|
||||
if tls_acceptor.is_none() || tcp_cnx.is_err() || sni_client_ca_keyid_map.is_none() {
|
||||
continue;
|
||||
}
|
||||
let (raw_stream, client_addr) = tcp_cnx.unwrap();
|
||||
let acceptor = tls_acceptor.clone().unwrap();
|
||||
let sni_cc_map = sni_client_ca_keyid_map.clone().unwrap();
|
||||
let server_clone = server.clone();
|
||||
let self_inner = self.clone();
|
||||
|
||||
|
|
@ -70,6 +81,13 @@ where
|
|||
if server_name.is_none(){
|
||||
Err(RpxyError::Proxy("No SNI is given".to_string()))
|
||||
} else {
|
||||
//////////////////////////////
|
||||
// Check client certificate
|
||||
// TODO: consider move this function to the layer of handle_request (L7) to return 403
|
||||
let client_certs = conn.peer_certificates();
|
||||
let client_certs_setting_for_sni = sni_cc_map.get(&server_name.clone().unwrap());
|
||||
check_client_authentication(client_certs, client_certs_setting_for_sni)?;
|
||||
//////////////////////////////
|
||||
// this immediately spawns another future to actually handle stream. so it is okay to introduce timeout for handshake.
|
||||
self_inner.client_serve(stream, server_clone, client_addr, server_name); // TODO: don't want to pass copied value...
|
||||
Ok(())
|
||||
|
|
@ -95,7 +113,8 @@ where
|
|||
break;
|
||||
}
|
||||
let server_crypto = server_crypto_rx.borrow().clone().unwrap();
|
||||
tls_acceptor = Some(TlsAcceptor::from(server_crypto));
|
||||
tls_acceptor = Some(TlsAcceptor::from(server_crypto.inner.clone()));
|
||||
sni_client_ca_keyid_map = Some(server_crypto.server_name_client_ca_keyids_map.clone());
|
||||
}
|
||||
else => break
|
||||
}
|
||||
|
|
@ -104,10 +123,10 @@ where
|
|||
}
|
||||
|
||||
#[cfg(feature = "http3")]
|
||||
async fn listener_service_h3(&self, mut server_crypto_rx: watch::Receiver<Option<Arc<ServerConfig>>>) -> Result<()> {
|
||||
async fn listener_service_h3(&self, mut server_crypto_rx: watch::Receiver<Option<Arc<ServerCrypto>>>) -> Result<()> {
|
||||
info!("Start UDP proxy serving with HTTP/3 request for configured host names");
|
||||
// first set as null config server
|
||||
let server_crypto = ServerConfig::builder()
|
||||
let rustls_server_config = ServerConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(Arc::new(tokio_rustls::rustls::server::ResolvesServerCertUsingSni::new()));
|
||||
|
|
@ -117,16 +136,17 @@ where
|
|||
.max_concurrent_bidi_streams(self.globals.h3_max_concurrent_bidistream)
|
||||
.max_concurrent_uni_streams(self.globals.h3_max_concurrent_unistream);
|
||||
|
||||
let mut server_config_h3 = QuicServerConfig::with_crypto(Arc::new(server_crypto));
|
||||
let mut server_config_h3 = QuicServerConfig::with_crypto(Arc::new(rustls_server_config));
|
||||
server_config_h3.transport = Arc::new(transport_config_quic);
|
||||
server_config_h3.concurrent_connections(self.globals.h3_max_concurrent_connections);
|
||||
let (endpoint, mut incoming) = Endpoint::server(server_config_h3, self.listening_on)?;
|
||||
|
||||
let mut server_crypto: Option<Arc<ServerConfig>> = None;
|
||||
let mut server_crypto: Option<Arc<ServerCrypto>> = None;
|
||||
let mut sni_client_ca_keyid_map: Option<Arc<SniKeyIdsMap>> = None;
|
||||
loop {
|
||||
tokio::select! {
|
||||
new_conn = incoming.next() => {
|
||||
if server_crypto.is_none() || new_conn.is_none() {
|
||||
if server_crypto.is_none() || new_conn.is_none() || sni_client_ca_keyid_map.is_none() {
|
||||
continue;
|
||||
}
|
||||
let mut conn = new_conn.unwrap();
|
||||
|
|
@ -152,7 +172,7 @@ where
|
|||
);
|
||||
// TODO: server_nameをここで出してどんどん深く投げていくのは効率が悪い。connecting -> connectionsの後でいいのでは?
|
||||
// TODO: 通常のTLSと同じenumか何かにまとめたい
|
||||
let fut = self.clone().connection_serve_h3(conn, new_server_name);
|
||||
let fut = self.clone().connection_serve_h3(conn, new_server_name, sni_client_ca_keyid_map.clone().unwrap());
|
||||
self.globals.runtime_handle.spawn(async move {
|
||||
// Timeout is based on underlying quic
|
||||
if let Err(e) = fut.await {
|
||||
|
|
@ -166,7 +186,8 @@ where
|
|||
}
|
||||
server_crypto = server_crypto_rx.borrow().clone();
|
||||
if server_crypto.is_some(){
|
||||
endpoint.set_server_config(Some(QuicServerConfig::with_crypto(server_crypto.clone().unwrap())));
|
||||
endpoint.set_server_config(Some(QuicServerConfig::with_crypto(server_crypto.clone().unwrap().inner.clone())));
|
||||
sni_client_ca_keyid_map = Some(server_crypto.clone().unwrap().server_name_client_ca_keyids_map.clone());
|
||||
}
|
||||
}
|
||||
else => break
|
||||
|
|
@ -177,7 +198,7 @@ where
|
|||
}
|
||||
|
||||
pub async fn start_with_tls(self, server: Http<LocalExecutor>) -> Result<()> {
|
||||
let (tx, rx) = watch::channel::<Option<Arc<ServerConfig>>>(None);
|
||||
let (tx, rx) = watch::channel::<Option<Arc<ServerCrypto>>>(None);
|
||||
#[cfg(not(feature = "http3"))]
|
||||
{
|
||||
select! {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ impl PathNameBytesExp {
|
|||
where
|
||||
I: std::slice::SliceIndex<[u8]>,
|
||||
{
|
||||
(&self.0).get(index)
|
||||
self.0.get(index)
|
||||
}
|
||||
pub fn starts_with(&self, needle: &Self) -> bool {
|
||||
self.0.starts_with(&needle.0)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue