jacs-cli 0.11.3

JACS CLI: command-line interface for JSON AI Communication Standard
Documentation
use assert_cmd::Command;
use serde_json::Value;
use tempfile::TempDir;

const TEST_PASSWORD: &str = "TestDocumentVerify!2026";

fn cmd() -> Command {
    let mut c = Command::cargo_bin("jacs").expect("jacs binary should exist");
    c.env("JACS_PRIVATE_KEY_PASSWORD", TEST_PASSWORD);
    c
}

fn sign_document(dir: &TempDir) -> Value {
    let input_path = dir.path().join("input.json");
    std::fs::write(
        &input_path,
        r#"{"message":"document verify forgery check"}"#,
    )
    .expect("write input");

    let output = cmd()
        .current_dir(dir.path())
        .args([
            "quickstart",
            "--algorithm",
            "ed25519",
            "--name",
            "document-verify-test",
            "--domain",
            "localhost",
            "--sign",
            "-f",
        ])
        .arg(&input_path)
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();

    serde_json::from_slice(&output).unwrap_or_else(|err| {
        panic!(
            "signed document stdout was not JSON: {}\n{}",
            err,
            String::from_utf8_lossy(&output)
        )
    })
}

fn replace_signature_with_forgery(document: &mut Value) {
    let signature = document["jacsSignature"]["signature"]
        .as_str()
        .expect("signature string");
    let mut forged_signature = signature.to_string();
    forged_signature.replace_range(0..1, if signature.starts_with('A') { "B" } else { "A" });
    document["jacsSignature"]["signature"] = Value::String(forged_signature);
}

fn recompute_document_hash(document: &mut Value) {
    document
        .as_object_mut()
        .expect("signed document object")
        .remove("jacsSha256");
    let canonical = jacs::protocol::canonicalize_json(document);
    let digest = jacs::crypt::hash::hash_string(&canonical);
    document
        .as_object_mut()
        .expect("signed document object")
        .insert("jacsSha256".to_string(), Value::String(digest));
}

#[test]
fn document_verify_rejects_forged_signature_even_when_hash_matches() {
    let dir = TempDir::new().expect("tmpdir");
    let signed = sign_document(&dir);
    let signed_path = dir.path().join("signed.json");
    std::fs::write(
        &signed_path,
        serde_json::to_vec_pretty(&signed).expect("signed json bytes"),
    )
    .expect("write signed document");

    cmd()
        .current_dir(dir.path())
        .args(["document", "verify", "-f"])
        .arg(&signed_path)
        .assert()
        .success();

    let mut forged = signed;
    replace_signature_with_forgery(&mut forged);
    recompute_document_hash(&mut forged);

    let forged_path = dir.path().join("forged.json");
    std::fs::write(
        &forged_path,
        serde_json::to_vec_pretty(&forged).expect("forged json bytes"),
    )
    .expect("write forged document");

    cmd()
        .current_dir(dir.path())
        .args(["document", "verify", "-f"])
        .arg(&forged_path)
        .assert()
        .failure();
}