est-ca 0.2.0

RFC 7030 Enrollment over Secure Transport (EST) — client, server, and an internal X.509 CA in pure Rust.
//! Negative-path tests: malformed CSR, auth failures, wrong content-type,
//! and missing-principal cases on the renewal endpoint.

#![cfg(all(feature = "server", feature = "client"))]

use std::sync::Arc;

use est_ca::auth::{AuthBackend, Principal};
use est_ca::ca::serial::InMemorySerialStore;
use est_ca::ca::{CertProfile, Issuer};
use est_ca::est::server::EstServer;
use est_ca::est::{CONTENT_TYPE_PKCS10, WELL_KNOWN_PREFIX};

/// Accepts only `user == "good-user" && pass == "right"`; every other
/// combination returns an error (403).
struct StrictAuth;

impl AuthBackend for StrictAuth {
    fn verify_bootstrap(&self, user: &str, pass: &str) -> Result<Principal, String> {
        if user == "good-user" && pass == "right" {
            Ok(Principal::new(user))
        } else {
            Err("bad credential".into())
        }
    }
}

async fn spawn() -> String {
    let issuer = Issuer::generate_self_signed(
        "error-paths test CA",
        CertProfile::client_auth_for_days(7),
        Arc::new(InMemorySerialStore::new()),
    )
    .unwrap();
    let app = EstServer::new(issuer, Arc::new(StrictAuth)).router();
    let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();
    tokio::spawn(async move {
        axum::Server::from_tcp(listener).unwrap().serve(app.into_make_service()).await.unwrap();
    });
    format!("http://{addr}")
}

fn url(base: &str, endpoint: &str) -> String {
    format!("{base}{WELL_KNOWN_PREFIX}{endpoint}")
}

#[tokio::test]
async fn simpleenroll_without_auth_returns_401() {
    let base = spawn().await;
    let http = reqwest::Client::new();
    let r = http
        .post(url(&base, "/simpleenroll"))
        .header(reqwest::header::CONTENT_TYPE, CONTENT_TYPE_PKCS10)
        .body(b"any-body".to_vec())
        .send()
        .await
        .unwrap();
    assert_eq!(r.status(), 401);
    assert!(
        r.headers().get(reqwest::header::WWW_AUTHENTICATE).is_some(),
        "401 must advertise Basic realm"
    );
}

#[tokio::test]
async fn simpleenroll_with_wrong_password_returns_403() {
    let base = spawn().await;
    let http = reqwest::Client::new();
    let r = http
        .post(url(&base, "/simpleenroll"))
        .basic_auth("good-user", Some("WRONG"))
        .header(reqwest::header::CONTENT_TYPE, CONTENT_TYPE_PKCS10)
        .body(b"any-body".to_vec())
        .send()
        .await
        .unwrap();
    assert_eq!(r.status(), 403);
}

#[tokio::test]
async fn simpleenroll_with_wrong_content_type_returns_415() {
    let base = spawn().await;
    let http = reqwest::Client::new();
    let r = http
        .post(url(&base, "/simpleenroll"))
        .basic_auth("good-user", Some("right"))
        .header(reqwest::header::CONTENT_TYPE, "application/json")
        .body(b"any-body".to_vec())
        .send()
        .await
        .unwrap();
    assert_eq!(r.status(), 415);
}

#[tokio::test]
async fn simpleenroll_with_malformed_csr_returns_400() {
    let base = spawn().await;
    let http = reqwest::Client::new();
    let r = http
        .post(url(&base, "/simpleenroll"))
        .basic_auth("good-user", Some("right"))
        .header(reqwest::header::CONTENT_TYPE, CONTENT_TYPE_PKCS10)
        .body(vec![0u8; 32]) // garbage bytes, not a valid PKCS#10
        .send()
        .await
        .unwrap();
    assert_eq!(r.status(), 400);
}

#[tokio::test]
async fn simplereenroll_without_principal_extension_returns_401() {
    let base = spawn().await;
    let http = reqwest::Client::new();
    let r = http
        .post(url(&base, "/simplereenroll"))
        .header(reqwest::header::CONTENT_TYPE, CONTENT_TYPE_PKCS10)
        .body(b"any-body".to_vec())
        .send()
        .await
        .unwrap();
    assert_eq!(
        r.status(),
        401,
        "renewal requires a Principal extension set by the TLS layer"
    );
}