papaleguas 0.0.9

ACME client
Documentation
use std::{error::Error, time::Duration};

use papaleguas::{AcmeClient, OrderStatus, PrivateKey};
use rand::thread_rng;
use reqwest::Certificate;
use serde_json::json;

type TestResult = Result<(), Box<dyn Error>>;

async fn pebble_http_client() -> Result<reqwest::Client, Box<dyn Error>> {
    let cert = tokio::fs::read("./tests/pebble/pebble.minica.pem").await?;
    let cert = Certificate::from_pem(&cert)?;
    let http = reqwest::Client::builder()
        .add_root_certificate(cert)
        .build()?;
    Ok(http)
}

async fn pebble_client() -> Result<AcmeClient, Box<dyn Error>> {
    let client = AcmeClient::builder()
        .http_client(pebble_http_client().await?)
        .build_with_directory_url("https://localhost:14000/dir")
        .await?;
    Ok(client)
}

#[tokio::test]
async fn test_directory_from_url() {
    let client = pebble_client().await;
    assert!(client.is_ok());
}

#[tokio::test]
async fn test_create_account() {
    let acme = pebble_client().await.unwrap();

    let account = acme
        .new_account()
        .with_auto_generated_ec_key()
        .contact("example@example.org")
        .contact("owner@example.org")
        .terms_of_service_agreed(true)
        .only_return_existing(false)
        .send()
        .await;

    assert!(account.is_ok());

    #[cfg(feature = "rsa")]
    let account = acme
        .new_account()
        .with_auto_generated_rsa_key()
        .contact("example@example.org")
        .contact("owner@example.org")
        .terms_of_service_agreed(true)
        .only_return_existing(false)
        .send()
        .await;

    #[cfg(feature = "rsa")]
    assert!(account.is_ok());

    let rng = rand::thread_rng();
    let key = PrivateKey::random_ec_key(rng);
    let account = acme
        .new_account()
        .private_key(key.clone())
        .contact("example@example.org")
        .terms_of_service_agreed(true)
        .only_return_existing(false)
        .send()
        .await;
    assert!(account.is_ok());
    let account = account.unwrap();

    let same_account = acme
        .new_account()
        .private_key(key.clone())
        .contact("example@example.org")
        .terms_of_service_agreed(true)
        .only_return_existing(false)
        .send()
        .await;
    assert!(same_account.is_ok());

    assert_eq!(account.kid(), same_account.unwrap().kid());

    let same_account = acme.existing_account_from_private_key(key).await;
    assert!(same_account.is_ok());
    assert_eq!(account.kid(), same_account.unwrap().kid());
}

#[tokio::test]
async fn test_error() -> TestResult {
    let acme = pebble_client().await?;

    let account = acme
        .new_account()
        .with_auto_generated_ec_key()
        .terms_of_service_agreed(false)
        .only_return_existing(false)
        .send()
        .await;

    assert!(account.is_err());

    Ok(())
}

#[tokio::test]
async fn test_retrive_account() -> TestResult {
    let acme = pebble_client().await?;

    let account = acme
        .new_account()
        .with_auto_generated_ec_key()
        .contact("example@example.org")
        .contact("owner@example.org")
        .terms_of_service_agreed(true)
        .only_return_existing(false)
        .send()
        .await?;

    let retrieved_account = acme
        .existing_account_from_private_key(account.key().clone())
        .await?;

    assert_eq!(account.kid(), retrieved_account.kid());

    Ok(())
}

#[tokio::test]
async fn test_order_list() -> TestResult {
    let acme = pebble_client().await?;

    let account = acme
        .new_account()
        .with_auto_generated_ec_key()
        .contact("example@example.org")
        .contact("owner@example.org")
        .terms_of_service_agreed(true)
        .only_return_existing(false)
        .send()
        .await?;

    for i in 1..=3 {
        account
            .new_order()
            .dns(format!("{i}.example.org"))
            .send()
            .await?;
    }

    assert_eq!(3, account.orders_urls().await?.len());

    Ok(())
}

#[tokio::test]
async fn test_create_order() -> TestResult {
    let acme = pebble_client().await?;

    let account = acme
        .new_account()
        .with_auto_generated_ec_key()
        .contact("example@example.org")
        .contact("owner@example.org")
        .terms_of_service_agreed(true)
        .only_return_existing(false)
        .send()
        .await?;

    let order = account
        .new_order()
        .dns("some-name.example.org")
        .send()
        .await;

    assert!(order.is_ok());

    Ok(())
}

#[tokio::test]
async fn test_generate_certificate_via_http01() -> TestResult {
    let acme = pebble_client().await?;

    let account = acme
        .new_account()
        .with_auto_generated_ec_key()
        .contact("example@example.org")
        .contact("owner@example.org")
        .terms_of_service_agreed(true)
        .only_return_existing(false)
        .send()
        .await?;

    let order = account
        .new_order()
        .dns("acme-http.example.org")
        .send()
        .await?;

    let challenge = order
        .authorizations()
        .await?
        .first()
        .and_then(|auth| auth.http01_challenge())
        .ok_or("Http challenge not found")?;

    let challenge_test_client = reqwest::Client::new();

    challenge_test_client
        .post("http://localhost:8055/add-a")
        .json(&json!({
            "host": "acme.example.org",
            "addresses": ["10.30.50.3"],
        }))
        .send()
        .await?;

    challenge_test_client
        .post("http://localhost:8055/add-http01")
        .json(&json!({
            "token": challenge.token(),
            "content": challenge.key_authorization()?,
        }))
        .send()
        .await?;

    challenge.validate().await?;

    let _cert = loop {
        let order = account.find_order(order.url()).await?;

        match order.status() {
            OrderStatus::Pending => {
                tokio::time::sleep(Duration::from_secs(3)).await;
            }
            OrderStatus::Ready => {
                let pkey = papaleguas::PrivateKey::random_ec_key(thread_rng());
                order.finalize(&pkey).await?;
            }
            OrderStatus::Processing => continue,
            OrderStatus::Valid => break order.certificate().await?,
            OrderStatus::Invalid => {
                return Err("Invalid order".into());
            }
        }
    };

    Ok(())
}

#[tokio::test]
async fn test_generate_certificate_via_tlsalpn01() -> TestResult {
    let acme = pebble_client().await?;

    let account = acme
        .new_account()
        .with_auto_generated_ec_key()
        .contact("example@example.org")
        .contact("owner@example.org")
        .terms_of_service_agreed(true)
        .only_return_existing(false)
        .send()
        .await?;

    let order = account
        .new_order()
        .dns("acme-tls.example.org")
        .send()
        .await?;

    let challenge = order
        .authorizations()
        .await?
        .first()
        .and_then(|auth| auth.tls_alpn01_challenge())
        .ok_or("Challenge not found")?;

    let challenge_test_client = reqwest::Client::new();

    challenge_test_client
        .post("http://localhost:8055/add-a")
        .json(&json!({
            "host": "acme-tls.example.org",
            "addresses": ["10.30.50.3"],
        }))
        .send()
        .await?;

    challenge_test_client
        .post("http://localhost:8055/add-tlsalpn01")
        .json(&json!({
            "host": "acme-tls.example.org",
            "content": challenge.key_authorization()?,
        }))
        .send()
        .await?;

    challenge.validate().await?;

    let pkey = papaleguas::PrivateKey::random_ec_key(thread_rng());
    let _cert = loop {
        let order = account.find_order(order.url()).await?;

        match order.status() {
            OrderStatus::Pending => {
                tokio::time::sleep(Duration::from_secs(3)).await;
            }
            OrderStatus::Ready => {
                order.finalize(&pkey).await?;
            }
            OrderStatus::Processing => continue,
            OrderStatus::Valid => break order.certificate().await?,
            OrderStatus::Invalid => {
                return Err("Invalid order".into());
            }
        }
    };

    Ok(())
}