mtls.rs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. //! `test(mtls)` — mTLS client authentication (plan todo 21).
  2. //!
  3. //! Covers the two RFC 8705 verifiers (chain-skipping `self_signed` vs
  4. //! `WebPkiClientVerifier` + client-CA bundle), thumbprint/DN matching,
  5. //! rejection of unknown/missing/ambiguous/cross-wired/expired identities,
  6. //! the trusted-proxy header mode, and the request-extension injection over
  7. //! a real TLS handshake through [`d_oauth::tls::serve_tls`].
  8. //!
  9. //! Test names contain `mtls` so
  10. //! `cargo nextest run -E 'test(mtls)'` selects exactly them. Bodies use
  11. //! `Result` + `?` (`unwrap`/`expect`/`panic` are denied workspace-wide).
  12. #[path = "mtls/certs.rs"]
  13. mod certs;
  14. use std::{
  15. net::{IpAddr, SocketAddr},
  16. sync::Arc,
  17. };
  18. use certs::FixtureError;
  19. use d_oauth::{
  20. db::client_repo::{self, Client, NewClient},
  21. error::{AppError, OAuthErrorCode},
  22. keys::{KeyId, KeyRing},
  23. protocol::mtls::{
  24. self, MtlsCnf, MtlsError, PROXY_CERT_HEADER, PROXY_VERIFY_HEADER, PROXY_VERIFY_SUCCESS,
  25. },
  26. };
  27. use secrecy::SecretString;
  28. mod common;
  29. /// The `oauth_clients` migration, embedded byte-for-byte from the repo.
  30. const OAUTH_CLIENTS_SQL: &str = include_str!("../migrations/1791072000_oauth_clients.sql");
  31. /// The forward-only client column additions the repository queries need.
  32. const CLIENT_DPOP_BOUND_SQL: &str = include_str!("../migrations/1791244800_client_dpop_bound.sql");
  33. /// Start a fresh database on the shared test Postgres and connect a pool
  34. /// to it (see `tests/common/mod.rs`). Each test migrates its own database,
  35. /// so parallel tests never share state.
  36. async fn mtls_postgres() -> Result<sqlx::PgPool, TestError> {
  37. common::postgres()
  38. .await
  39. .map_err(|error| TestError::Setup(error.to_string()))
  40. }
  41. /// Apply the embedded migration script exactly as the migrator would.
  42. async fn mtls_migrate(pool: &sqlx::PgPool) -> Result<(), TestError> {
  43. sqlx::raw_sql(OAUTH_CLIENTS_SQL).execute(pool).await?;
  44. sqlx::raw_sql(CLIENT_DPOP_BOUND_SQL).execute(pool).await?;
  45. sqlx::raw_sql(
  46. "ALTER TABLE oauth_clients ALTER COLUMN client_name DROP NOT NULL;
  47. ALTER TABLE oauth_clients
  48. ADD COLUMN client_name_enc bytea NULL,
  49. ADD COLUMN contacts_enc bytea NULL,
  50. ADD COLUMN request_uris_enc bytea NULL,
  51. ADD COLUMN jwks_uri_enc bytea NULL,
  52. ADD COLUMN software_id_enc bytea NULL,
  53. ADD COLUMN tls_client_auth_subject_dn_enc bytea NULL,
  54. ADD COLUMN tls_client_auth_cert_thumbprint_enc bytea NULL,
  55. ADD COLUMN tls_client_auth_subject_dn_index text NULL,
  56. ADD COLUMN tls_client_auth_cert_thumbprint_index text NULL;
  57. CREATE UNIQUE INDEX ux_oauth_clients_tls_subject_dn_index
  58. ON oauth_clients (tls_client_auth_subject_dn_index)
  59. WHERE tls_client_auth_subject_dn_index IS NOT NULL;
  60. CREATE UNIQUE INDEX ux_oauth_clients_tls_cert_thumbprint_index
  61. ON oauth_clients (tls_client_auth_cert_thumbprint_index)
  62. WHERE tls_client_auth_cert_thumbprint_index IS NOT NULL;",
  63. )
  64. .execute(pool)
  65. .await?;
  66. Ok(())
  67. }
  68. /// Test PII key ring for the encrypted client projection.
  69. fn test_keys() -> Result<KeyRing, TestError> {
  70. Ok(KeyRing::from_parts(
  71. vec![(KeyId::new("v1"), SecretString::from("mtls-test-pepper"))],
  72. vec![(KeyId::new("v1"), SecretString::from("mtls-test-totp"))],
  73. vec![(KeyId::new("v1"), SecretString::from("mtls-test-pii"))],
  74. )?)
  75. }
  76. /// `x5t#S256` of the self-signed fixture (re-minted 2026-10-06, valid to
  77. /// 2036-10-03; see `tests/mtls/certs.rs` if fixtures are re-minted).
  78. const SELF_THUMBPRINT: &str = "UoFOLf7P9QP8478uT1qx-2pZyBJQDdNlbbLyopxl8cA";
  79. /// `x5t#S256` of the CA-signed fixture (re-minted 2026-10-06).
  80. const PKI_THUMBPRINT: &str = "VSFBS4-7neqMcHVsx-dcc28rEEemzFvmeqtZSE3xbK8";
  81. /// Subject of the self-signed fixture in [`mtls::subject_dn`] rendering.
  82. const SELF_DN: &str = "CN=mtls-test-client, O=Example";
  83. /// Subject of the CA-signed fixture in [`mtls::subject_dn`] rendering.
  84. const PKI_DN: &str = "CN=mtls-pki-client, O=Example";
  85. /// Test-body error.
  86. #[derive(Debug, thiserror::Error)]
  87. enum TestError {
  88. /// The shared test service is unreachable (usually: compose not started).
  89. #[error("test setup: {0}")]
  90. Setup(String),
  91. /// A sqlx operation failed.
  92. #[error("database error: {0}")]
  93. Database(#[from] sqlx::Error),
  94. /// An mTLS operation failed.
  95. #[error("mtls error: {0}")]
  96. Mtls(#[from] MtlsError),
  97. /// An application error escaped where success was required.
  98. #[error("app error: {0}")]
  99. App(#[from] AppError),
  100. /// A test key ring was rejected.
  101. #[error("key error: {0}")]
  102. Keys(#[from] d_oauth::keys::KeyRingError),
  103. /// A fixture did not decode.
  104. #[error("fixture error: {0}")]
  105. Fixture(#[from] FixtureError),
  106. /// A rustls operation failed.
  107. #[error("tls error: {0}")]
  108. Tls(#[from] rustls::Error),
  109. /// An I/O operation failed.
  110. #[error("io error: {0}")]
  111. Io(#[from] std::io::Error),
  112. /// An HTTP value was invalid.
  113. #[error("http error: {0}")]
  114. Http(#[from] axum::http::Error),
  115. /// A spawned task failed.
  116. #[error("task error: {0}")]
  117. Task(#[from] tokio::task::JoinError),
  118. /// An address did not parse.
  119. #[error("address error: {0}")]
  120. Addr(#[from] std::net::AddrParseError),
  121. /// A DNS name was invalid.
  122. #[error("dns error: {0}")]
  123. Dns(#[from] rustls::pki_types::InvalidDnsNameError),
  124. /// A response was not UTF-8.
  125. #[error("encoding error: {0}")]
  126. Encoding(#[from] std::string::FromUtf8Error),
  127. }
  128. type TestResult = Result<(), TestError>;
  129. /// Insert one mTLS client row with a runtime query (no sqlx macros, so the
  130. /// offline query cache is untouched).
  131. async fn insert_mtls_client(
  132. pool: &sqlx::PgPool,
  133. client_id: &str,
  134. method: &str,
  135. dn: Option<&str>,
  136. thumbprint: Option<&str>,
  137. ) -> TestResult {
  138. sqlx::query(
  139. "INSERT INTO oauth_clients (id, client_id, client_id_issued_at, client_name,
  140. grant_types, response_types, scope, token_endpoint_auth_method,
  141. tls_client_auth_subject_dn, tls_client_auth_cert_thumbprint,
  142. created_at, updated_at)
  143. VALUES ($1, $2, now(), 'mTLS Test',
  144. ARRAY['client_credentials'], ARRAY['token'], 'api:read', $3, $4, $5,
  145. now(), now())",
  146. )
  147. .bind(uuid::Uuid::new_v4())
  148. .bind(client_id)
  149. .bind(method)
  150. .bind(dn)
  151. .bind(thumbprint)
  152. .execute(pool)
  153. .await?;
  154. Ok(())
  155. }
  156. /// Minimal in-memory client for the pure (DB-free) selector tests.
  157. fn fixture_client(method: &str, dn: Option<&str>, thumbprint: Option<&str>) -> Client {
  158. Client {
  159. id: uuid::Uuid::nil(),
  160. client_id: "mtls-fixture".to_owned(),
  161. client_secret_expires_at: None,
  162. client_id_issued_at: time::OffsetDateTime::UNIX_EPOCH,
  163. client_name: "mTLS Fixture".to_owned(),
  164. grant_types: Vec::new(),
  165. response_types: Vec::new(),
  166. scope: String::new(),
  167. token_endpoint_auth_method: method.to_owned(),
  168. jwks: None,
  169. jwks_uri: None,
  170. tls_client_auth_subject_dn: dn.map(str::to_owned),
  171. tls_client_auth_cert_thumbprint: thumbprint.map(str::to_owned),
  172. request_uris: None,
  173. contacts: None,
  174. software_id: None,
  175. dpop_bound_access_tokens: false,
  176. active: true,
  177. created_at: time::OffsetDateTime::UNIX_EPOCH,
  178. updated_at: time::OffsetDateTime::UNIX_EPOCH,
  179. }
  180. }
  181. fn is_invalid_client(result: Result<MtlsCnf, MtlsError>) -> bool {
  182. matches!(
  183. result.map(|_| ()).map_err(MtlsError::into_app_error),
  184. Err(AppError::OAuth {
  185. code: OAuthErrorCode::InvalidClient,
  186. ..
  187. })
  188. )
  189. }
  190. /// The thumbprint matches the `openssl`-computed vector (RFC 8705 §2.1).
  191. #[test]
  192. fn mtls_thumbprint_matches_openssl_vector() -> TestResult {
  193. let der = certs::cert_der(certs::CLIENT_SELF_CERT_PEM)?;
  194. assert_eq!(mtls::thumbprint_b64url(der.as_ref()), SELF_THUMBPRINT);
  195. let pki = certs::cert_der(certs::CLIENT_PKI_CERT_PEM)?;
  196. assert_eq!(mtls::thumbprint_b64url(pki.as_ref()), PKI_THUMBPRINT);
  197. Ok(())
  198. }
  199. /// The subject DN renders in RFC 4514 certificate-attribute order.
  200. #[test]
  201. fn mtls_subject_dn_renders_rfc4514() -> TestResult {
  202. let der = certs::cert_der(certs::CLIENT_SELF_CERT_PEM)?;
  203. assert_eq!(mtls::subject_dn(der.as_ref())?, SELF_DN);
  204. let pki = certs::cert_der(certs::CLIENT_PKI_CERT_PEM)?;
  205. assert_eq!(mtls::subject_dn(pki.as_ref())?, PKI_DN);
  206. Ok(())
  207. }
  208. /// A registered self-signed certificate binds `cnf.x5t#S256`.
  209. #[tokio::test]
  210. async fn mtls_registered_thumbprint_binds_cnf() -> TestResult {
  211. let pool = mtls_postgres().await?;
  212. mtls_migrate(&pool).await?;
  213. insert_mtls_client(
  214. &pool,
  215. "mtls-self",
  216. "self_signed_tls_client_auth",
  217. None,
  218. Some(SELF_THUMBPRINT),
  219. )
  220. .await?;
  221. let client = client_repo::find_active_by_client_id(&pool, &test_keys()?, "mtls-self")
  222. .await?
  223. .ok_or(MtlsError::UnknownClient)?;
  224. let leaf = certs::cert_der(certs::CLIENT_SELF_CERT_PEM)?;
  225. let cnf = mtls::authenticate(&pool, &test_keys()?, &client, Some(&leaf)).await?;
  226. assert_eq!(cnf.x5t_s256, SELF_THUMBPRINT);
  227. assert_eq!(cnf.claim()["x5t#S256"], SELF_THUMBPRINT);
  228. Ok(())
  229. }
  230. /// A CA-issued certificate binds via subject DN under `tls_client_auth`.
  231. #[tokio::test]
  232. async fn mtls_pki_subject_dn_binds_cnf() -> TestResult {
  233. let pool = mtls_postgres().await?;
  234. mtls_migrate(&pool).await?;
  235. insert_mtls_client(&pool, "mtls-pki", "tls_client_auth", Some(PKI_DN), None).await?;
  236. let client = client_repo::find_active_by_client_id(&pool, &test_keys()?, "mtls-pki")
  237. .await?
  238. .ok_or(MtlsError::UnknownClient)?;
  239. let leaf = certs::cert_der(certs::CLIENT_PKI_CERT_PEM)?;
  240. let cnf = mtls::authenticate(&pool, &test_keys()?, &client, Some(&leaf)).await?;
  241. assert_eq!(cnf.x5t_s256, PKI_THUMBPRINT);
  242. Ok(())
  243. }
  244. /// An unregistered certificate is rejected as `invalid_client`.
  245. #[tokio::test]
  246. async fn mtls_unregistered_thumbprint_rejected_as_invalid_client() -> TestResult {
  247. let pool = mtls_postgres().await?;
  248. mtls_migrate(&pool).await?;
  249. insert_mtls_client(
  250. &pool,
  251. "mtls-self",
  252. "self_signed_tls_client_auth",
  253. None,
  254. Some(SELF_THUMBPRINT),
  255. )
  256. .await?;
  257. let client = client_repo::find_active_by_client_id(&pool, &test_keys()?, "mtls-self")
  258. .await?
  259. .ok_or(MtlsError::UnknownClient)?;
  260. let rogue = certs::cert_der(certs::CLIENT_ROGUE_CERT_PEM)?;
  261. let result = mtls::authenticate(&pool, &test_keys()?, &client, Some(&rogue)).await;
  262. assert!(matches!(result, Err(MtlsError::UnknownCertificate)));
  263. assert!(is_invalid_client(result));
  264. Ok(())
  265. }
  266. /// A missing certificate rejects every mTLS-registered client.
  267. #[tokio::test]
  268. async fn mtls_missing_cert_rejects_mtls_clients() -> TestResult {
  269. let pool = mtls_postgres().await?;
  270. mtls_migrate(&pool).await?;
  271. let pinned = fixture_client("self_signed_tls_client_auth", None, Some(SELF_THUMBPRINT));
  272. let result = mtls::authenticate(&pool, &test_keys()?, &pinned, None).await;
  273. assert!(matches!(result, Err(MtlsError::MissingCertificate)));
  274. assert!(is_invalid_client(result));
  275. let pki = fixture_client("tls_client_auth", Some(PKI_DN), None);
  276. let result = mtls::authenticate(&pool, &test_keys()?, &pki, None).await;
  277. assert!(matches!(result, Err(MtlsError::MissingCertificate)));
  278. assert!(is_invalid_client(result));
  279. Ok(())
  280. }
  281. /// RFC 8705 §2: the presented certificate must match the identity registered
  282. /// for the requesting `client_id`. A certificate enrolled for one client must
  283. /// not authenticate another, in either direction, and no `cnf` may be bound.
  284. #[tokio::test]
  285. async fn mtls_cross_wire_certificate_rejected_for_other_client() -> TestResult {
  286. let pool = mtls_postgres().await?;
  287. mtls_migrate(&pool).await?;
  288. let keys = test_keys()?;
  289. insert_mtls_client(
  290. &pool,
  291. "mtls-xwire-a",
  292. "self_signed_tls_client_auth",
  293. None,
  294. Some(SELF_THUMBPRINT),
  295. )
  296. .await?;
  297. insert_mtls_client(
  298. &pool,
  299. "mtls-xwire-b",
  300. "self_signed_tls_client_auth",
  301. None,
  302. Some(PKI_THUMBPRINT),
  303. )
  304. .await?;
  305. insert_mtls_client(&pool, "mtls-xwire-c", "tls_client_auth", Some(PKI_DN), None).await?;
  306. let leaf_a = certs::cert_der(certs::CLIENT_SELF_CERT_PEM)?;
  307. let leaf_b = certs::cert_der(certs::CLIENT_PKI_CERT_PEM)?;
  308. // A's certificate while authenticating as B: B's thumbprint pin differs.
  309. let result = mtls::authenticate_by_id(&pool, &keys, "mtls-xwire-b", Some(&leaf_a)).await;
  310. assert!(matches!(result, Err(MtlsError::UnknownCertificate)));
  311. assert!(is_invalid_client(result));
  312. // B's certificate while authenticating as A: A's thumbprint pin differs.
  313. let result = mtls::authenticate_by_id(&pool, &keys, "mtls-xwire-a", Some(&leaf_b)).await;
  314. assert!(matches!(result, Err(MtlsError::UnknownCertificate)));
  315. assert!(is_invalid_client(result));
  316. // A's certificate while authenticating as the subject-DN client C.
  317. let result = mtls::authenticate_by_id(&pool, &keys, "mtls-xwire-c", Some(&leaf_a)).await;
  318. assert!(matches!(result, Err(MtlsError::UnknownCertificate)));
  319. assert!(is_invalid_client(result));
  320. // Controls: each client still binds with its own certificate, so the
  321. // rejections above are the cross-wire and not an over-broad guard.
  322. assert_eq!(
  323. mtls::authenticate_by_id(&pool, &keys, "mtls-xwire-a", Some(&leaf_a))
  324. .await?
  325. .x5t_s256,
  326. SELF_THUMBPRINT
  327. );
  328. assert_eq!(
  329. mtls::authenticate_by_id(&pool, &keys, "mtls-xwire-b", Some(&leaf_b))
  330. .await?
  331. .x5t_s256,
  332. PKI_THUMBPRINT
  333. );
  334. assert_eq!(
  335. mtls::authenticate_by_id(&pool, &keys, "mtls-xwire-c", Some(&leaf_b))
  336. .await?
  337. .x5t_s256,
  338. PKI_THUMBPRINT
  339. );
  340. Ok(())
  341. }
  342. /// The registered method is part of the credential: `tls_client_auth` is
  343. /// matched by subject DN, `self_signed_tls_client_auth` by exact thumbprint,
  344. /// and neither accepts the other method's identity.
  345. #[tokio::test]
  346. async fn mtls_auth_methods_not_interchangeable() -> TestResult {
  347. let pool = mtls_postgres().await?;
  348. mtls_migrate(&pool).await?;
  349. let keys = test_keys()?;
  350. insert_mtls_client(
  351. &pool,
  352. "mtls-method-pinned",
  353. "self_signed_tls_client_auth",
  354. None,
  355. Some(SELF_THUMBPRINT),
  356. )
  357. .await?;
  358. insert_mtls_client(
  359. &pool,
  360. "mtls-method-dn",
  361. "tls_client_auth",
  362. Some(PKI_DN),
  363. None,
  364. )
  365. .await?;
  366. let self_leaf = certs::cert_der(certs::CLIENT_SELF_CERT_PEM)?;
  367. let pki_leaf = certs::cert_der(certs::CLIENT_PKI_CERT_PEM)?;
  368. // A self-signed certificate is not the identity registered for the
  369. // tls_client_auth client.
  370. let result = mtls::authenticate_by_id(&pool, &keys, "mtls-method-dn", Some(&self_leaf)).await;
  371. assert!(matches!(result, Err(MtlsError::UnknownCertificate)));
  372. assert!(is_invalid_client(result));
  373. // A CA-issued certificate is not the thumbprint pinned for the
  374. // self_signed_tls_client_auth client.
  375. let result = mtls::authenticate_by_id(&pool, &keys, "mtls-method-pinned", Some(&pki_leaf)).await;
  376. assert!(matches!(result, Err(MtlsError::UnknownCertificate)));
  377. assert!(is_invalid_client(result));
  378. // Controls: each method accepts exactly its own registered identity.
  379. assert_eq!(
  380. mtls::authenticate_by_id(&pool, &keys, "mtls-method-pinned", Some(&self_leaf))
  381. .await?
  382. .x5t_s256,
  383. SELF_THUMBPRINT
  384. );
  385. assert_eq!(
  386. mtls::authenticate_by_id(&pool, &keys, "mtls-method-dn", Some(&pki_leaf))
  387. .await?
  388. .x5t_s256,
  389. PKI_THUMBPRINT
  390. );
  391. Ok(())
  392. }
  393. /// RFC 8705 §2 and RFC 5280 §6.3.3: a certificate outside its validity
  394. /// window authenticates nobody, even when its thumbprint or subject DN is
  395. /// registered byte-for-byte; it must never bind a `cnf`.
  396. #[tokio::test]
  397. async fn mtls_expired_certificate_never_binds_cnf() -> TestResult {
  398. let pool = mtls_postgres().await?;
  399. mtls_migrate(&pool).await?;
  400. let keys = test_keys()?;
  401. let expired = certs::cert_der(certs::CLIENT_EXPIRED_CERT_PEM)?;
  402. let expired_thumbprint = mtls::thumbprint_b64url(expired.as_ref());
  403. let expired_dn = mtls::subject_dn(expired.as_ref())?;
  404. insert_mtls_client(
  405. &pool,
  406. "mtls-expired-pinned",
  407. "self_signed_tls_client_auth",
  408. None,
  409. Some(&expired_thumbprint),
  410. )
  411. .await?;
  412. insert_mtls_client(
  413. &pool,
  414. "mtls-expired-dn",
  415. "tls_client_auth",
  416. Some(&expired_dn),
  417. None,
  418. )
  419. .await?;
  420. let pinned = client_repo::find_active_by_client_id(&pool, &keys, "mtls-expired-pinned")
  421. .await?
  422. .ok_or(MtlsError::UnknownClient)?;
  423. assert_eq!(
  424. pinned.tls_client_auth_cert_thumbprint.as_deref(),
  425. Some(expired_thumbprint.as_str())
  426. );
  427. let by_dn = client_repo::find_active_by_client_id(&pool, &keys, "mtls-expired-dn")
  428. .await?
  429. .ok_or(MtlsError::UnknownClient)?;
  430. assert_eq!(
  431. by_dn.tls_client_auth_subject_dn.as_deref(),
  432. Some(expired_dn.as_str())
  433. );
  434. // The registered identity matches; only the validity window rejects, and
  435. // no `cnf` may be produced for either method.
  436. let result = mtls::authenticate_by_id(&pool, &keys, "mtls-expired-pinned", Some(&expired)).await;
  437. assert!(matches!(result, Err(MtlsError::ExpiredCertificate)));
  438. assert!(is_invalid_client(result));
  439. let result = mtls::authenticate_by_id(&pool, &keys, "mtls-expired-dn", Some(&expired)).await;
  440. assert!(matches!(result, Err(MtlsError::ExpiredCertificate)));
  441. assert!(is_invalid_client(result));
  442. Ok(())
  443. }
  444. /// An encrypted subject-DN row matches after decryption; an unknown DN
  445. /// fails closed; a corrupt `tls_client_auth_subject_dn_enc` surfaces as
  446. /// 503 `temporarily_unavailable`, never a flattened `invalid_client`.
  447. #[tokio::test]
  448. async fn mtls_subject_dn_select() -> TestResult {
  449. let pool = mtls_postgres().await?;
  450. mtls_migrate(&pool).await?;
  451. let keys = test_keys()?;
  452. client_repo::insert(
  453. &pool,
  454. &keys,
  455. false,
  456. &NewClient {
  457. id: uuid::Uuid::new_v4(),
  458. client_id: "mtls-enc-dn".to_owned(),
  459. client_secret_hash: None,
  460. client_secret_expires_at: None,
  461. client_name: "mTLS Encrypted DN".to_owned(),
  462. grant_types: vec!["client_credentials".to_owned()],
  463. response_types: vec!["token".to_owned()],
  464. scope: "api:read".to_owned(),
  465. token_endpoint_auth_method: "tls_client_auth".to_owned(),
  466. jwks: None,
  467. jwks_uri: None,
  468. tls_client_auth_subject_dn: Some(PKI_DN.to_owned()),
  469. tls_client_auth_cert_thumbprint: None,
  470. request_uris: None,
  471. contacts: None,
  472. software_id: None,
  473. dpop_bound_access_tokens: false,
  474. registration_access_token_hash: None,
  475. },
  476. )
  477. .await?;
  478. let client = client_repo::find_active_by_client_id(&pool, &keys, "mtls-enc-dn")
  479. .await?
  480. .ok_or(MtlsError::UnknownClient)?;
  481. assert_eq!(client.tls_client_auth_subject_dn.as_deref(), Some(PKI_DN));
  482. let clients = [client.clone()];
  483. assert_eq!(
  484. mtls::select_by_subject_dn(&clients, PKI_DN)?.client_id,
  485. "mtls-enc-dn"
  486. );
  487. assert!(matches!(
  488. mtls::select_by_subject_dn(&clients, "CN=nobody, O=Example"),
  489. Err(MtlsError::UnknownCertificate)
  490. ));
  491. let leaf = certs::cert_der(certs::CLIENT_PKI_CERT_PEM)?;
  492. let cnf = mtls::authenticate(&pool, &keys, &client, Some(&leaf)).await?;
  493. assert_eq!(cnf.x5t_s256, PKI_THUMBPRINT);
  494. let corrupt: &[u8] = b"corrupt-blob";
  495. sqlx::query("UPDATE oauth_clients SET tls_client_auth_subject_dn_enc = $1 WHERE client_id = $2")
  496. .bind(corrupt)
  497. .bind("mtls-enc-dn")
  498. .execute(&pool)
  499. .await?;
  500. match client_repo::find_active_by_client_id(&pool, &keys, "mtls-enc-dn").await {
  501. Ok(_) => {
  502. return Err(TestError::Setup(
  503. "corrupt subject_dn_enc decrypted".to_owned(),
  504. ));
  505. }
  506. Err(error) => {
  507. assert!(matches!(error, AppError::PiiDecrypt));
  508. let app = MtlsError::from(error).into_app_error();
  509. assert_eq!(app.code(), OAuthErrorCode::TemporarilyUnavailable);
  510. assert_eq!(app.status(), axum::http::StatusCode::SERVICE_UNAVAILABLE);
  511. }
  512. }
  513. Ok(())
  514. }
  515. /// A subject DN shared by two clients is rejected as ambiguous.
  516. #[test]
  517. fn mtls_subject_dn_ambiguity_rejected() -> TestResult {
  518. let first = fixture_client("tls_client_auth", Some(PKI_DN), None);
  519. let second = fixture_client("tls_client_auth", Some(PKI_DN), None);
  520. let clients = [first, second];
  521. let result = mtls::select_by_subject_dn(&clients, PKI_DN);
  522. assert!(matches!(result, Err(MtlsError::AmbiguousSubjectDn)));
  523. assert!(is_invalid_client(result.map(|_| MtlsCnf {
  524. x5t_s256: String::new()
  525. })));
  526. let lone = fixture_client("tls_client_auth", Some(PKI_DN), None);
  527. let clients = [lone];
  528. assert_eq!(
  529. mtls::select_by_subject_dn(&clients, PKI_DN)?.client_id,
  530. "mtls-fixture"
  531. );
  532. assert!(matches!(
  533. mtls::select_by_subject_dn(&clients, "CN=nobody, O=Example"),
  534. Err(MtlsError::UnknownCertificate)
  535. ));
  536. Ok(())
  537. }
  538. /// Build request parts carrying the given proxy headers.
  539. fn mtls_proxy_parts(pairs: &[(&str, &str)]) -> Result<axum::http::request::Parts, TestError> {
  540. let mut builder = axum::http::Request::builder().uri("/token");
  541. for (name, value) in pairs {
  542. builder = builder.header(*name, *value);
  543. }
  544. Ok(builder.body(())?.into_parts().0)
  545. }
  546. /// A forged proxy header is ignored without a valid verification signal,
  547. /// and always ignored when no proxy is trusted.
  548. #[test]
  549. fn mtls_forged_proxy_header_ignored_without_success_signal() -> TestResult {
  550. let der_b64 = certs::der_b64(certs::CLIENT_SELF_CERT_PEM)?;
  551. let proxy: Option<IpAddr> = Some("127.0.0.1".parse()?);
  552. // No trusted proxy: even a SUCCESS-signed header is ignored.
  553. let parts = mtls_proxy_parts(&[
  554. (PROXY_VERIFY_HEADER, PROXY_VERIFY_SUCCESS),
  555. (PROXY_CERT_HEADER, &der_b64),
  556. ])?;
  557. assert!(mtls::presented_leaf(&parts, None)?.is_none());
  558. // Trusted proxy but no signal: ignored.
  559. let parts = mtls_proxy_parts(&[(PROXY_CERT_HEADER, &der_b64)])?;
  560. assert!(mtls::presented_leaf(&parts, proxy)?.is_none());
  561. // Trusted proxy with a failed signal: ignored.
  562. let parts = mtls_proxy_parts(&[
  563. (PROXY_VERIFY_HEADER, "FAILED"),
  564. (PROXY_CERT_HEADER, &der_b64),
  565. ])?;
  566. assert!(mtls::presented_leaf(&parts, proxy)?.is_none());
  567. // SUCCESS signal but no certificate: nothing to trust.
  568. let parts = mtls_proxy_parts(&[(PROXY_VERIFY_HEADER, PROXY_VERIFY_SUCCESS)])?;
  569. assert!(mtls::presented_leaf(&parts, proxy)?.is_none());
  570. Ok(())
  571. }
  572. /// A SUCCESS-signed header is honored only with `trusted_proxy`, and the
  573. /// direct listener strips inbound headers before routing.
  574. #[test]
  575. fn mtls_proxy_success_signal_accepted_and_stripped() -> TestResult {
  576. let der_b64 = certs::der_b64(certs::CLIENT_SELF_CERT_PEM)?;
  577. let proxy: Option<IpAddr> = Some("127.0.0.1".parse()?);
  578. let parts = mtls_proxy_parts(&[
  579. (PROXY_VERIFY_HEADER, PROXY_VERIFY_SUCCESS),
  580. (PROXY_CERT_HEADER, &der_b64),
  581. ])?;
  582. let leaf = mtls::presented_leaf(&parts, proxy)?.ok_or(MtlsError::MissingCertificate)?;
  583. assert_eq!(mtls::thumbprint_b64url(leaf.as_ref()), SELF_THUMBPRINT);
  584. // A SUCCESS-signed garbage header fails closed instead of binding.
  585. let parts = mtls_proxy_parts(&[
  586. (PROXY_VERIFY_HEADER, PROXY_VERIFY_SUCCESS),
  587. (PROXY_CERT_HEADER, "!!!"),
  588. ])?;
  589. assert!(matches!(
  590. mtls::presented_leaf(&parts, proxy),
  591. Err(MtlsError::MalformedCertificate)
  592. ));
  593. // Stripping removes both headers, so downstream sees no certificate.
  594. let mut parts = mtls_proxy_parts(&[
  595. (PROXY_VERIFY_HEADER, PROXY_VERIFY_SUCCESS),
  596. (PROXY_CERT_HEADER, &der_b64),
  597. ])?;
  598. mtls::strip_proxy_headers(&mut parts.headers);
  599. assert!(!parts.headers.contains_key(PROXY_CERT_HEADER));
  600. assert!(!parts.headers.contains_key(PROXY_VERIFY_HEADER));
  601. assert!(mtls::presented_leaf(&parts, proxy)?.is_none());
  602. Ok(())
  603. }
  604. /// Handler echo for the handshake test: the bound thumbprint, or `no-cert`.
  605. async fn mtls_thumbprint_handler(req: axum::http::Request<axum::body::Body>) -> String {
  606. match req.extensions().get::<d_oauth::tls::PeerCertificates>() {
  607. Some(peer) => peer.chain().first().map_or_else(
  608. || "empty".to_owned(),
  609. |der| mtls::thumbprint_b64url(der.as_ref()),
  610. ),
  611. None => "no-cert".to_owned(),
  612. }
  613. }
  614. /// Raw HTTPS GET over a rustls client config, returning the response body.
  615. async fn mtls_get(
  616. addr: SocketAddr,
  617. client_cert: Option<(
  618. Vec<rustls::pki_types::CertificateDer<'static>>,
  619. rustls::pki_types::PrivateKeyDer<'static>,
  620. )>,
  621. ) -> Result<String, TestError> {
  622. use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
  623. let mut roots = rustls::RootCertStore::empty();
  624. roots.add(certs::cert_der(certs::SERVER_CERT_PEM)?)?;
  625. let builder = rustls::ClientConfig::builder().with_root_certificates(roots);
  626. let config = match client_cert {
  627. Some((chain, key)) => builder.with_client_auth_cert(chain, key)?,
  628. None => builder.with_no_client_auth(),
  629. };
  630. let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
  631. let stream = tokio::net::TcpStream::connect(addr).await?;
  632. let server_name = rustls::pki_types::ServerName::try_from("localhost")?.to_owned();
  633. let mut tls = connector.connect(server_name, stream).await?;
  634. tls
  635. .write_all(b"GET / HTTP/1.1\r\nhost: localhost\r\nconnection: close\r\n\r\n")
  636. .await?;
  637. let mut raw = Vec::new();
  638. tls.read_to_end(&mut raw).await?;
  639. let text = String::from_utf8(raw)?;
  640. Ok(text.split("\r\n\r\n").last().unwrap_or("").to_owned())
  641. }
  642. /// The extension-injection mechanism works over a real TLS handshake: a
  643. /// self-signed client (no CA involved) binds, and a cert-less connection on
  644. /// the same listener is served with no extension.
  645. #[tokio::test]
  646. async fn mtls_real_tls_handshake_injects_peer_extension() -> TestResult {
  647. let app = axum::Router::new().route("/", axum::routing::get(mtls_thumbprint_handler));
  648. let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
  649. let addr = listener.local_addr()?;
  650. let config = mtls::server_config(
  651. certs::SERVER_CERT_PEM.as_bytes(),
  652. certs::SERVER_KEY_PEM.as_bytes(),
  653. mtls::self_signed_verifier(),
  654. )?;
  655. let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
  656. let server = tokio::spawn(async move { d_oauth::tls::serve_tls(listener, acceptor, app).await });
  657. let chain = vec![certs::cert_der(certs::CLIENT_SELF_CERT_PEM)?];
  658. let key = certs::private_key(certs::CLIENT_SELF_KEY_PEM)?;
  659. assert_eq!(mtls_get(addr, Some((chain, key))).await?, SELF_THUMBPRINT);
  660. assert_eq!(mtls_get(addr, None).await?, "no-cert");
  661. server.abort();
  662. Ok(())
  663. }
  664. /// One TLS byte-roundtrip with the given client identity.
  665. async fn mtls_roundtrip(addr: SocketAddr, cert_pem: &str, key_pem: &str) -> Result<u8, TestError> {
  666. use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
  667. let mut roots = rustls::RootCertStore::empty();
  668. roots.add(certs::cert_der(certs::SERVER_CERT_PEM)?)?;
  669. let config = rustls::ClientConfig::builder()
  670. .with_root_certificates(roots)
  671. .with_client_auth_cert(
  672. vec![certs::cert_der(cert_pem)?],
  673. certs::private_key(key_pem)?,
  674. )?;
  675. let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
  676. let stream = tokio::net::TcpStream::connect(addr).await?;
  677. let server_name = rustls::pki_types::ServerName::try_from("localhost")?.to_owned();
  678. let mut tls = connector.connect(server_name, stream).await?;
  679. tls.write_all(b"Z").await?;
  680. let mut byte = [0_u8; 1];
  681. tls.read_exact(&mut byte).await?;
  682. Ok(byte[0])
  683. }
  684. /// The PKI verifier chains to the client CA: a CA-signed client completes
  685. /// the handshake while a self-signed impostor is rejected mid-handshake.
  686. #[tokio::test]
  687. async fn mtls_pki_verifier_accepts_ca_signed_rejects_self_signed() -> TestResult {
  688. use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
  689. let verifier = mtls::pki_verifier(certs::CA_CERT_PEM.as_bytes())?;
  690. let config = mtls::server_config(
  691. certs::SERVER_CERT_PEM.as_bytes(),
  692. certs::SERVER_KEY_PEM.as_bytes(),
  693. verifier,
  694. )?;
  695. let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
  696. let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
  697. let addr = listener.local_addr()?;
  698. let server = tokio::spawn(async move {
  699. let mut completed = 0;
  700. for _ in 0..2 {
  701. let (stream, _) = listener.accept().await?;
  702. let mut tls = acceptor.accept(stream).await?;
  703. let mut byte = [0_u8; 1];
  704. tls.read_exact(&mut byte).await?;
  705. tls.write_all(&byte).await?;
  706. completed += 1;
  707. }
  708. Ok::<u32, TestError>(completed)
  709. });
  710. assert_eq!(
  711. mtls_roundtrip(addr, certs::CLIENT_PKI_CERT_PEM, certs::CLIENT_PKI_KEY_PEM).await?,
  712. b'Z'
  713. );
  714. assert!(
  715. mtls_roundtrip(
  716. addr,
  717. certs::CLIENT_SELF_CERT_PEM,
  718. certs::CLIENT_SELF_KEY_PEM
  719. )
  720. .await
  721. .is_err()
  722. );
  723. assert!(server.await?.is_err());
  724. Ok(())
  725. }