mtls.rs 25 KB

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