mtls.rs 25 KB

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