micro-tss 0.1.0

A simple implementation of a Tatsu Signing Server.
#[macro_use]
extern crate tracing;
#[macro_use]
extern crate anyhow;

mod config;
mod maps;
mod signers;
mod ticket;

use anyhow::Context;
use axum::{
    body::{Body, Bytes, HttpBody},
    http::{Request, StatusCode},
    routing::post,
    Router,
};
use clap::{arg, command, value_parser};
use config::{Config, RuntimeConfig};
use plist::Value as PlistValue;
use signers::Signers;
use std::{collections::BTreeMap, path::PathBuf};
use tracing_subscriber::FmtSubscriber;

#[tokio::main]
#[instrument]
async fn main() -> anyhow::Result<()> {
    let matches = command!()
        .arg(
            arg!(-c --config <FILE> "Sets path to a config file.")
                .value_parser(value_parser!(PathBuf)),
        )
        .get_matches();

    let config_path: &PathBuf = matches.get_one("config").expect("missing required arg");
    let Config { setup, runtime } =
        plist::from_file(config_path).context("while loading config file")?;

    FmtSubscriber::builder().init();

    let signers = Signers::load(&setup.signers).await?;

    let app = Router::new().route(
        "/TSS/controller",
        post(move |req: Request<Body>| async move {
            handle(&runtime, req, &signers).await.map_err(|e| {
                println!("{e}");
                StatusCode::BAD_REQUEST
            })
        }),
    );

    axum::Server::bind(&setup.listen_addr)
        .serve(app.into_make_service())
        .await?;

    Ok(())
}

const DEFAULT_USER_AGENT: &str = "libauthinstall-836.0.0.111.3";

async fn handle(
    config: &RuntimeConfig,
    mut req: Request<Body>,
    signers: &Signers,
) -> anyhow::Result<Bytes> {
    let data = req.data().await.ok_or_else(|| anyhow!("no data"))??;
    let req: BTreeMap<String, PlistValue> = plist::from_bytes(&data)?;

    if config.forward_local_policy && req.contains_key("Ap,LocalPolicy") {
        let client = reqwest::Client::new();
        let resp = client
            .post("http://gs.apple.com/TSS/controller?action=2")
            .header("Content-Type", "text/xml; charset=\"utf-8\"")
            .header(
                "User-Agent",
                config.user_agent.as_deref().unwrap_or(DEFAULT_USER_AGENT),
            )
            .body(data)
            .send()
            .await?;
        return Ok(resp.bytes().await?);
    }

    let mut resp = BTreeMap::new();
    resp.insert("@ServerVersion".to_string(), "2.1.0".into());

    if req.get("@ApImg4Ticket") == Some(&PlistValue::Boolean(true)) {
        ticket::append_ap_img4_ticket(&req, &mut resp, signers, "ApImg4Ticket")?;
    }

    if req.get("@Cryptex1,Ticket") == Some(&PlistValue::Boolean(true)) {
        ticket::append_ap_img4_ticket(&req, &mut resp, signers, "Cryptex1,Ticket")?;
    }

    let mut bytes = Vec::new();
    bytes.extend_from_slice(b"STATUS=0&MESSAGE=SUCCESS&REQUEST_STRING=");
    plist::to_writer_xml(&mut bytes, &resp)?;

    Ok(bytes.into())
}