peer.rs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. //! Peer-certificate request helper for mTLS router tests (plan todo 7).
  2. //!
  3. //! Drives the REAL production router ([`d_oauth::routes::build_router`],
  4. //! unmodified) in-process via `oneshot`, injecting the verified peer chain
  5. //! the TLS accept loop would insert ([`d_oauth::tls::PeerCertificates`]).
  6. //! Both peer-identity extensions the stack reads are ALWAYS inserted
  7. //! (`ConnectInfo` for the limiter, `MockConnectInfo` for the `ConnectInfo`
  8. //! extractor; fixed `127.0.0.1:40001`, mirroring `src/routes.rs` `drive`),
  9. //! so the rate-limit layer never fails closed. No TLS listener is needed.
  10. //!
  11. //! Included by the mTLS grant suite as `#[path = "e2e/peer.rs"] mod peer;`
  12. //! alongside `#[path = "e2e/harness.rs"] mod harness;` (the happy-path
  13. //! check below reaches `World` as `crate::harness::World`, the same shape
  14. //! every existing e2e suite uses).
  15. use std::{net::SocketAddr, sync::Arc};
  16. use axum::{
  17. body::Body,
  18. extract::{ConnectInfo, connect_info::MockConnectInfo},
  19. http::Request,
  20. response::Response,
  21. };
  22. use d_oauth::{routes::build_router, state::AppState, tls::PeerCertificates};
  23. use rustls::pki_types::CertificateDer;
  24. use tower::ServiceExt as _;
  25. // PEM fixtures live in `tests/mtls/certs.rs`, one directory up from here.
  26. #[path = "../mtls/certs.rs"]
  27. mod certs;
  28. pub use certs::FixtureError;
  29. /// `x5t#S256` of the CA-signed fixture (copied from `tests/mtls.rs`).
  30. pub const PKI_THUMBPRINT: &str = "VSFBS4-7neqMcHVsx-dcc28rEEemzFvmeqtZSE3xbK8";
  31. /// Subject of the CA-signed fixture in `mtls::subject_dn` rendering
  32. /// (copied from `tests/mtls.rs`).
  33. pub const PKI_DN: &str = "CN=mtls-pki-client, O=Example";
  34. /// Build the `PEER_CHAIN`: the CA-signed client leaf
  35. /// ([`CLIENT_PKI_CERT_PEM`](certs::CLIENT_PKI_CERT_PEM)) chained to the
  36. /// client CA bundle ([`CA_CERT_PEM`](certs::CA_CERT_PEM)), the same chain
  37. /// the TLS accept loop would hand the router for the PKI fixture.
  38. pub fn peer_chain() -> Result<Vec<CertificateDer<'static>>, FixtureError> {
  39. Ok(vec![
  40. certs::cert_der(certs::CLIENT_PKI_CERT_PEM)?,
  41. certs::cert_der(certs::CA_CERT_PEM)?,
  42. ])
  43. }
  44. /// Drive one request through the production router with peer extensions.
  45. ///
  46. /// When `peer` is `Some`, its chain is inserted as [`PeerCertificates`];
  47. /// when `None`, the extension is absent (the "no client certificate"
  48. /// shape). Both `ConnectInfo` and `MockConnectInfo` are always inserted so
  49. /// the rate-limit layer attributes the fixed loopback peer instead of
  50. /// failing closed with 503.
  51. pub async fn oneshot_with_peer(
  52. state: AppState,
  53. mut request: Request<Body>,
  54. peer: Option<Vec<CertificateDer<'static>>>,
  55. ) -> Response {
  56. let addr = SocketAddr::from(([127, 0, 0, 1], 40_001));
  57. if let Some(chain) = peer {
  58. request
  59. .extensions_mut()
  60. .insert(PeerCertificates(Arc::new(chain)));
  61. }
  62. request.extensions_mut().insert(ConnectInfo(addr));
  63. request.extensions_mut().insert(MockConnectInfo(addr));
  64. match build_router(state).oneshot(request).await {
  65. Ok(response) => response,
  66. Err(error) => match error {},
  67. }
  68. }
  69. /// Happy-path check for the helper. Files under `tests/e2e/` are included
  70. /// via `#[path]` (never standalone targets), so this module only compiles
  71. /// when a test target includes `peer.rs` — exactly how todo 14 will use it
  72. /// (`#[path = "e2e/peer.rs"] mod peer;` next to `mod harness;`).
  73. #[cfg(test)]
  74. mod tests {
  75. use axum::http::{Request, StatusCode};
  76. use d_oauth::protocol::mtls;
  77. use super::{PKI_DN, PKI_THUMBPRINT, oneshot_with_peer, peer_chain};
  78. /// Test-body result (no `unwrap`/`expect` under the deny lints).
  79. type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
  80. /// `GET /jwks.json` through the helper with the given peer evidence.
  81. async fn get_jwks(
  82. peer: Option<Vec<rustls::pki_types::CertificateDer<'static>>>,
  83. ) -> TestResult<StatusCode> {
  84. let world = crate::harness::World::start(crate::harness::open_rate_limit()).await?;
  85. let request = Request::builder()
  86. .uri("/jwks.json")
  87. .body(axum::body::Body::empty())?;
  88. Ok(
  89. oneshot_with_peer(world.state.clone(), request, peer)
  90. .await
  91. .status(),
  92. )
  93. }
  94. /// The helper serves `GET /jwks.json` with 200 both with and without a
  95. /// peer chain (the chain only matters to mTLS-authenticated grants), and
  96. /// the built chain matches the pinned PKI constants.
  97. #[tokio::test]
  98. async fn peer_helper_serves_jwks_with_and_without_chain() -> TestResult {
  99. let chain = peer_chain()?;
  100. if mtls::thumbprint_b64url(chain[0].as_ref()) != PKI_THUMBPRINT {
  101. return Err("peer leaf thumbprint must match PKI_THUMBPRINT".into());
  102. }
  103. if mtls::subject_dn(chain[0].as_ref())? != PKI_DN {
  104. return Err("peer leaf subject must match PKI_DN".into());
  105. }
  106. if get_jwks(None).await? != StatusCode::OK {
  107. return Err("GET /jwks.json without a peer must be 200".into());
  108. }
  109. if get_jwks(Some(chain)).await? != StatusCode::OK {
  110. return Err("GET /jwks.json with a peer chain must be 200".into());
  111. }
  112. Ok(())
  113. }
  114. }