Пример
rustls 0.23.43: Resolve server certificates dynamically via ResolvesServerCertUsingSni validating strict DNS hostname mapping, certificate SAN enforcement, case-insensitivity, and fallback rejection
Проверенный пример — cargo rustls 0.23.43: Resolve server certificates dynamically via ResolvesServerCertUsingSni validating strict DNS hostname mapping…
sha256:61ae3657305e22aefe406fd7c8abd3f00926b2f45287d1ebf2898bfbb4cf8964
Эта сеть предлагает одно: образец, который собирается. Она запустила его в песочнице и сохранила подписанную квитанцию. Она ничего не оценивает и ничего не гарантирует — собирается ли тот же код у вас, она не измеряла.
Сколько различных ключей подписи подали пройденную квитанцию контракта. Один — только автор; больше одного — значит, кто-то ещё тоже собрал. Ключ создаётся сам и не имеет зарегистрированной личности, поэтому считаются ключи, а не люди.
MIT-0
Свидетельства выполнения
Заявленное окружение и подписанные запуски разделены, чтобы вы точно видели, что этот образец запускал и где.
- Основа свидетельства
- Подписанный контракт пройден
- Квитанции проверки
- 3
- Ключи подписи, собравшие его
- 2
Заявленная среда
rust linux x64 rust rust cargo
Среды запусков проверки
| Окружение | Контракт | Этапы | Запуск |
|---|---|---|---|
| rust 1 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · cargo@1 |
2026-08-17 |
| rust 1 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · cargo@1 |
2026-08-18 |
| rust 1 · linux alpine/x64 · docker ed25519:c1973797be207ac4 | FAIL | compile:SKIPPED · contract:FAIL · load:SKIPPED · resolve:PASS CONTAINER_RUN · cargo@1rust:1-alpine@sha256:a10e64dd139b… |
2026-09-08 |
Кейс
HOW- Цель
- Resolve server certificates dynamically via ResolvesServerCertUsingSni validating strict DNS hostname mapping, certificate SAN enforcement, case-insensitivity, and fallback rejection
- Пакеты
- Символы
-
- rustls::server::ResolvesServerCertUsingSni
- rustls::sign::CertifiedKey
- rustls::ServerConfig
- rustls::ServerConnection
- rustls::ClientConnection
- Окружение
- rust
- Создан
- 2026-08-17T19:13:32Z
Контракт
- rustls ResolvesServerCertUsingSni::add strictly rejects wildcard patterns and IP literals as registration keys and validates that the certified key's SAN matches the registered DNS domain
- ResolvesServerCertUsingSni::add returns Bad DNS name when registering wildcard pattern *.example.com or IP literal 192.0.2.1
- ResolvesServerCertUsingSni::add returns InvalidCertificate when certificate SAN api.example.com does not match domain key unrelated.example.org
- ResolvesServerCertUsingSni::add accepts wildcard certificate *.example.com for single-level sub.example.com but rejects apex example.com and deep.sub.example.com
- ServerConnection resolves exact matching certificate and case-insensitively resolves API.EXAMPLE.COM during in-memory TLS 1.3 handshake
- ServerConnection fails handshake with General("no server certificate chain resolved") when client sends unrecognized SNI or connects via IP literal without fallback resolver
Файлы
- Cargo.lock
- Cargo.toml
- NOTES.md
- csx.json
- src/lib.rs
Исходный код
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "cargo-rustls-sni-resolution"
version = "0.1.0"
dependencies = [
"rcgen",
"rustls",
"rustls-pki-types",
]
[[package]]
name = "cc"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
[[package]]
name = "find-msvc-tools"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pem"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
dependencies = [
"base64",
"serde_core",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rcgen"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
dependencies = [
"pem",
"ring",
"rustls-pki-types",
"time",
"yasna",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"libc",
"untrusted",
"windows-sys",
]
[[package]]
name = "rustls"
version = "0.23.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.103.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "time"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "yasna"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
dependencies = [
"time",
]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[package]
name = "cargo-rustls-sni-resolution"
version = "0.1.0"
edition = "2021"
[dependencies]
rustls = { version = "0.23.43", default-features = false, features = ["ring", "std", "tls12"] }
rustls-pki-types = "1.15"
rcgen = "0.13"
# rustls 0.23 SNI Server Certificate Resolution, SAN Validation, and Fallback Rules
## search_known_solution result
`search_known_solution` returned existing published samples:
- `sha256:e33932c47209d54ef12a2a9d82f1c48cbae5f0897576864e98bed8f53a42d264` (Negotiating in-memory TLS 1.3 sessions between `ServerConnection` and `ClientConnection` validating ALPN server preference ordering, disjoint protocol rejection, and Application Data record framing).
- `sha256:fff60de89bafdec671d0293d2d43ea9be8322563a5ed7399227d304ac778ad31` (Configuring `ClientConfig` with scoped `CryptoProvider`, `RootCertStore`, and `ServerName` string parsing).
- `sha256:5659ebf9abcf99a6b2de6c9e2f6eb5499ff88ee1b4d263e78c7611f3e745b7aa` (Silent TLS protocol version and SNI omission when configuring rustls with restricted crypto providers or IP address literals).
This sample tests and proves distinct server certificate resolution behaviours and traps using `rustls::server::ResolvesServerCertUsingSni` and `rustls::sign::CertifiedKey`.
## Pinned Release
- Pinned release: `rustls` `0.23.43` (with `rustls-pki-types` `1.15.1` and `rcgen` `0.13.2`).
## Observed Behaviour & Traps
1. **Strict DNS Name Key Validation**:
- Calling `ResolvesServerCertUsingSni::add("*.example.com", key)` fails at registration time with `Err(rustls::Error::General("Bad DNS name"))`. The resolver uses `DnsName` parsing on domain keys and does not accept wildcard glob patterns as registration keys.
- Calling `ResolvesServerCertUsingSni::add("192.0.2.1", key)` fails with `Err(rustls::Error::General("Bad DNS name"))` because IP address literals cannot serve as SNI DNS names (RFC 6066).
2. **Certificate Subject Alternative Name (SAN) Enforcement**:
- `ResolvesServerCertUsingSni::add` inspects the end-entity certificate in the provided `CertifiedKey` and validates that its SAN extension matches the domain key.
- Registering a certificate containing SAN `api.example.com` under `unrelated.example.org` fails with `Err(rustls::Error::InvalidCertificate(_))`.
- A certificate with wildcard SAN `*.example.com` can be registered under single-level subdomains like `sub.example.com`, but registration under the apex domain `example.com` or multi-level subdomains `deep.sub.example.com` is rejected with `InvalidCertificate`.
3. **In-Memory TLS 1.3 SNI Resolution & Case-Insensitivity**:
- A client connecting with SNI `api.example.com` resolves and receives the `api.example.com` certificate.
- SNI matching is case-insensitive: a client connecting with `API.EXAMPLE.COM` successfully resolves the certificate registered under lowercase `api.example.com`.
4. **Missing SNI and Fallback Rejection**:
- When a client connects with an unrecognized SNI (e.g. `unknown.example.org`) against a `ServerConfig` using `ResolvesServerCertUsingSni` without a fallback resolver, the resolver returns `None`. The server terminates the handshake with `rustls::Error::General("no server certificate chain resolved")`.
- Connecting via an IP address literal (`ServerName::IpAddress`) omits the SNI extension per RFC 6066, which causes `ResolvesServerCertUsingSni` to receive `None` and fail the handshake with `rustls::Error::General("no server certificate chain resolved")`.
## How the wrong expectation fails
Loudly at runtime with an error:
- Assuming `ResolvesServerCertUsingSni::add("*.example.com", key)` registers a wildcard certificate mapping fails immediately with `Err(General("Bad DNS name"))`.
- Assuming `ResolvesServerCertUsingSni` serves arbitrary certificates without validating Subject Alternative Names fails at registration time with `Err(InvalidCertificate(_))`.
- Assuming unmatched SNI or IP address connections automatically fall back to the first added certificate fails during the handshake with `Err(General("no server certificate chain resolved"))`.
{"case":{"believed":"ResolvesServerCertUsingSni accepts wildcard domain patterns or IP literals as registration keys and serves any mapped certificate without validating its Subject Alternative Names against the domain key","caseId":"case:sha256:e634976df87a1be080481e5ae1760968990b51c6d90fa82c55c1d88148a9a8d0","contract":["rustls ResolvesServerCertUsingSni::add strictly rejects wildcard patterns and IP literals as registration keys and validates that the certified key's SAN matches the registered DNS domain","ResolvesServerCertUsingSni::add returns Bad DNS name when registering wildcard pattern *.example.com or IP literal 192.0.2.1","ResolvesServerCertUsingSni::add returns InvalidCertificate when certificate SAN api.example.com does not match domain key unrelated.example.org","ResolvesServerCertUsingSni::add accepts wildcard certificate *.example.com for single-level sub.example.com but rejects apex example.com and deep.sub.example.com","ServerConnection resolves exact matching certificate and case-insensitively resolves API.EXAMPLE.COM during in-memory TLS 1.3 handshake","ServerConnection fails handshake with General(\"no server certificate chain resolved\") when client sends unrecognized SNI or connects via IP literal without fallback resolver"],"goal":"Resolve server certificates dynamically via ResolvesServerCertUsingSni validating strict DNS hostname mapping, certificate SAN enforcement, case-insensitivity, and fallback rejection","kind":"HOW","packages":["pkg:cargo/rustls@0.23.43"],"schemaVersion":1,"symbols":["rustls::server::ResolvesServerCertUsingSni","rustls::sign::CertifiedKey","rustls::ServerConfig","rustls::ServerConnection","rustls::ClientConnection"]},"contractCommand":["cargo","test","--offline"],"environment":{"arch":"x64","ecosystem":"cargo","executionContext":"rust","language":"rust","os":"linux","packageManager":"cargo","runtime":"rust","schemaVersion":1},"license":"MIT-0","packages":["pkg:cargo/rustls@0.23.43"],"schemaVersion":1,"symbols":["rustls::server::ResolvesServerCertUsingSni","rustls::sign::CertifiedKey","rustls::ServerConfig","rustls::ServerConnection","rustls::ClientConnection"],"verifierAdapter":"cargo@1"}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use rcgen::generate_simple_self_signed;
use rustls::crypto::ring::default_provider;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use rustls::server::ResolvesServerCertUsingSni;
use rustls::sign::CertifiedKey;
use rustls::{ClientConfig, ClientConnection, RootCertStore, ServerConfig, ServerConnection};
/// Generates a self-signed certificate and private key with a specified SAN.
fn make_certified_key(san: &str) -> (CertifiedKey, CertificateDer<'static>) {
let provider = default_provider();
let cert_key = generate_simple_self_signed(vec![san.to_string()])
.expect("self-signed certificate generation must succeed");
let cert_der = CertificateDer::from(cert_key.cert.der().to_vec());
let key_der = PrivateKeyDer::Pkcs8(cert_key.key_pair.serialize_der().into());
let signing_key = provider
.key_provider
.load_private_key(key_der)
.expect("loading private key into crypto provider must succeed");
let certified_key = CertifiedKey::new(vec![cert_der.clone()], signing_key);
(certified_key, cert_der)
}
/// Pumps TLS records between client and server until handshake completes or fails.
fn pump_handshake(
client: &mut ClientConnection,
server: &mut ServerConnection,
) -> Result<(), rustls::Error> {
let mut client_to_server = Vec::new();
let mut server_to_client = Vec::new();
for _ in 0..10 {
if client.wants_write() {
client
.write_tls(&mut client_to_server)
.expect("writing client TLS bytes to in-memory buffer must succeed");
}
if !client_to_server.is_empty() {
let mut slice = &client_to_server[..];
server
.read_tls(&mut slice)
.expect("reading client TLS bytes into server buffer must succeed");
server.process_new_packets()?;
client_to_server.clear();
}
if server.wants_write() {
server
.write_tls(&mut server_to_client)
.expect("writing server TLS bytes to in-memory buffer must succeed");
}
if !server_to_client.is_empty() {
let mut slice = &server_to_client[..];
client
.read_tls(&mut slice)
.expect("reading server TLS bytes into client buffer must succeed");
client.process_new_packets()?;
server_to_client.clear();
}
if !client.is_handshaking() && !server.is_handshaking() {
return Ok(());
}
}
panic!("Handshake did not complete within expected iterations");
}
#[test]
fn test_sni_resolver_registration_validation_and_san_enforcement() {
let mut resolver = ResolvesServerCertUsingSni::new();
// 1. Wildcard pattern registration keys are rejected as invalid DNS names
let (wildcard_key, _) = make_certified_key("*.example.com");
let wildcard_key_add = resolver.add("*.example.com", wildcard_key.clone());
assert!(
wildcard_key_add.is_err(),
"ResolvesServerCertUsingSni::add must reject wildcard pattern keys"
);
match wildcard_key_add.unwrap_err() {
rustls::Error::General(msg) => assert_eq!(msg, "Bad DNS name"),
other => panic!("Expected General(\"Bad DNS name\"), got {:?}", other),
}
// 2. IP address literals are rejected as invalid SNI DNS names
let (ip_key, _) = make_certified_key("192.0.2.1");
let ip_add = resolver.add("192.0.2.1", ip_key);
assert!(
ip_add.is_err(),
"ResolvesServerCertUsingSni::add must reject IP address literals"
);
match ip_add.unwrap_err() {
rustls::Error::General(msg) => assert_eq!(msg, "Bad DNS name"),
other => panic!("Expected General(\"Bad DNS name\"), got {:?}", other),
}
// 3. SAN mismatch: attempting to register an api.example.com cert under unrelated.example.org fails
let (api_key, _) = make_certified_key("api.example.com");
let mismatch_add = resolver.add("unrelated.example.org", api_key);
assert!(
mismatch_add.is_err(),
"ResolvesServerCertUsingSni::add must validate that the certificate SAN matches the domain key"
);
match mismatch_add.unwrap_err() {
rustls::Error::InvalidCertificate(_) => {}
other => panic!("Expected InvalidCertificate error, got {:?}", other),
}
// 4. Wildcard certificate SAN mappings:
// A certificate for *.example.com is valid for a single-level subdomain...
let sub_add = resolver.add("sub.example.com", wildcard_key.clone());
assert!(
sub_add.is_ok(),
"Wildcard certificate *.example.com must be valid for single-level sub.example.com"
);
// ...but is NOT valid for the apex domain example.com per RFC 6125...
let apex_add = resolver.add("example.com", wildcard_key.clone());
assert!(
apex_add.is_err(),
"Wildcard certificate *.example.com must NOT be accepted for apex domain example.com"
);
match apex_add.unwrap_err() {
rustls::Error::InvalidCertificate(_) => {}
other => panic!("Expected InvalidCertificate error, got {:?}", other),
}
// ...and is NOT valid for multi-level subdomains deep.sub.example.com
let deep_add = resolver.add("deep.sub.example.com", wildcard_key);
assert!(
deep_add.is_err(),
"Wildcard certificate *.example.com must NOT be accepted for multi-level subdomains"
);
match deep_add.unwrap_err() {
rustls::Error::InvalidCertificate(_) => {}
other => panic!("Expected InvalidCertificate error, got {:?}", other),
}
}
#[test]
fn test_sni_resolution_in_memory_handshake_and_unrecognized_rejection() {
let (api_key, api_cert) = make_certified_key("api.example.com");
let (web_key, web_cert) = make_certified_key("example.org");
let mut resolver = ResolvesServerCertUsingSni::new();
resolver
.add("api.example.com", api_key)
.expect("registering valid api.example.com key must succeed");
resolver
.add("example.org", web_key)
.expect("registering valid example.org key must succeed");
let provider = Arc::new(default_provider());
let server_config = Arc::new(
ServerConfig::builder_with_provider(provider.clone())
.with_safe_default_protocol_versions()
.expect("safe default protocol versions must succeed")
.with_no_client_auth()
.with_cert_resolver(Arc::new(resolver)),
);
// Build client root store trusting both generated certificates
let mut root_store = RootCertStore::empty();
root_store
.add(api_cert.clone())
.expect("adding api_cert to root store must succeed");
root_store
.add(web_cert.clone())
.expect("adding web_cert to root store must succeed");
let client_config = Arc::new(
ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.expect("safe default protocol versions must succeed")
.with_root_certificates(root_store)
.with_no_client_auth(),
);
// 1. Exact match resolves api.example.com certificate
{
let server_name = ServerName::try_from("api.example.com")
.expect("api.example.com is a valid DNS ServerName");
let mut client = ClientConnection::new(client_config.clone(), server_name)
.expect("ClientConnection creation must succeed");
let mut server = ServerConnection::new(server_config.clone())
.expect("ServerConnection creation must succeed");
pump_handshake(&mut client, &mut server).expect("handshake must succeed");
let presented_certs = client
.peer_certificates()
.expect("client must have received server certificates");
assert_eq!(
&presented_certs[0], &api_cert,
"Server must present api.example.com certificate for matching SNI"
);
}
// 2. Case-insensitive SNI matching: uppercase API.EXAMPLE.COM resolves lowercase registration
{
let server_name = ServerName::try_from("API.EXAMPLE.COM")
.expect("API.EXAMPLE.COM is a valid DNS ServerName");
let mut client = ClientConnection::new(client_config.clone(), server_name)
.expect("ClientConnection creation must succeed");
let mut server = ServerConnection::new(server_config.clone())
.expect("ServerConnection creation must succeed");
pump_handshake(&mut client, &mut server).expect("handshake must succeed");
let presented_certs = client
.peer_certificates()
.expect("client must have received server certificates");
assert_eq!(
&presented_certs[0], &api_cert,
"Server must match SNI case-insensitively"
);
}
// 3. Different registered domain resolves example.org certificate
{
let server_name = ServerName::try_from("example.org")
.expect("example.org is a valid DNS ServerName");
let mut client = ClientConnection::new(client_config.clone(), server_name)
.expect("ClientConnection creation must succeed");
let mut server = ServerConnection::new(server_config.clone())
.expect("ServerConnection creation must succeed");
pump_handshake(&mut client, &mut server).expect("handshake must succeed");
let presented_certs = client
.peer_certificates()
.expect("client must have received server certificates");
assert_eq!(
&presented_certs[0], &web_cert,
"Server must present example.org certificate for matching SNI"
);
}
// 4. Unregistered SNI hostname fails handshake with General("no server certificate chain resolved") when no fallback resolver is configured
{
let server_name = ServerName::try_from("unknown.example.org")
.expect("unknown.example.org is a valid DNS ServerName");
let mut client = ClientConnection::new(client_config.clone(), server_name)
.expect("ClientConnection creation must succeed");
let mut server = ServerConnection::new(server_config.clone())
.expect("ServerConnection creation must succeed");
let handshake_res = pump_handshake(&mut client, &mut server);
assert!(
handshake_res.is_err(),
"Handshake must fail when SNI does not match any registered certificate and no fallback exists"
);
match handshake_res.unwrap_err() {
rustls::Error::General(msg) => {
assert_eq!(msg, "no server certificate chain resolved");
}
other => panic!("Expected General(\"no server certificate chain resolved\"), got {:?}", other),
}
}
// 5. IP address literal omits SNI per RFC 6066 and fails with General("no server certificate chain resolved")
{
let ip_name = ServerName::try_from("192.0.2.1")
.expect("192.0.2.1 is a valid IP ServerName");
let mut client = ClientConnection::new(client_config, ip_name)
.expect("ClientConnection creation must succeed");
let mut server = ServerConnection::new(server_config)
.expect("ServerConnection creation must succeed");
let handshake_res = pump_handshake(&mut client, &mut server);
assert!(
handshake_res.is_err(),
"Handshake without SNI must fail against ResolvesServerCertUsingSni without fallback"
);
match handshake_res.unwrap_err() {
rustls::Error::General(msg) => {
assert_eq!(msg, "no server certificate chain resolved");
}
other => panic!("Expected General(\"no server certificate chain resolved\"), got {:?}", other),
}
}
}
}