use anyhow::Result;
use rsa::pkcs8::EncodePrivateKey;
use rsa::traits::PublicKeyParts;
use rsa::{Pkcs1v15Sign, RsaPrivateKey};
use sha2::{Digest, Sha256};
fn der_len(n: usize) -> Vec<u8> {
if n < 0x80 {
return vec![n as u8];
}
let mut b = n.to_be_bytes().to_vec();
while b.len() > 1 && b[0] == 0 {
b.remove(0);
}
let mut o = vec![0x80 | b.len() as u8];
o.extend(b);
o
}
fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
let mut o = vec![tag];
o.extend(der_len(content.len()));
o.extend_from_slice(content);
o
}
fn seq(c: &[u8]) -> Vec<u8> {
tlv(0x30, c)
}
fn set(c: &[u8]) -> Vec<u8> {
tlv(0x31, c)
}
fn ctx(n: u8, c: &[u8]) -> Vec<u8> {
tlv(0xA0 | n, c) }
fn oid(parts: &[u32]) -> Vec<u8> {
let mut b = vec![(parts[0] * 40 + parts[1]) as u8];
for &p in &parts[2..] {
let mut stack = vec![(p & 0x7f) as u8];
let mut v = p >> 7;
while v > 0 {
stack.push((v & 0x7f) as u8 | 0x80);
v >>= 7;
}
stack.reverse();
b.extend(stack);
}
tlv(0x06, &b)
}
fn int_bytes(mut b: Vec<u8>) -> Vec<u8> {
if b.is_empty() {
b.push(0);
}
if b[0] & 0x80 != 0 {
b.insert(0, 0);
}
tlv(0x02, &b)
}
fn bit_string(c: &[u8]) -> Vec<u8> {
let mut v = vec![0u8]; v.extend_from_slice(c);
tlv(0x03, &v)
}
fn utf8(s: &str) -> Vec<u8> {
tlv(0x0c, s.as_bytes())
}
fn null() -> Vec<u8> {
vec![0x05, 0x00]
}
pub struct Csr {
pub der: Vec<u8>,
pub key_pem: String,
}
fn build_sid_ext_value(sid: &str) -> Vec<u8> {
let inner_octet = tlv(0x04, sid.as_bytes()); let value_explicit = ctx(0, &inner_octet); let type_id = oid(&[1, 3, 6, 1, 4, 1, 311, 25, 2, 1]);
let other_name = ctx(0, &[type_id, value_explicit].concat()); seq(&other_name) }
pub fn build_csr(subject_cn: &str, upn: Option<&str>) -> Result<Csr> {
build_csr_with_sid_ext(subject_cn, upn, None)
}
pub fn build_csr_with_sid_ext(
subject_cn: &str,
upn: Option<&str>,
target_sid: Option<&str>,
) -> Result<Csr> {
let mut rng = rand::thread_rng();
let key = RsaPrivateKey::new(&mut rng, 2048)?;
build_csr_from_key(&key, subject_cn, upn, target_sid)
}
pub fn build_csr_from_key(
key: &RsaPrivateKey,
subject_cn: &str,
upn: Option<&str>,
target_sid: Option<&str>,
) -> Result<Csr> {
let pk = key.to_public_key();
let rsa_pub = seq(&[
int_bytes(pk.n().to_bytes_be()),
int_bytes(pk.e().to_bytes_be()),
]
.concat());
let alg_rsa = seq(&[oid(&[1, 2, 840, 113549, 1, 1, 1]), null()].concat());
let spki = seq(&[alg_rsa, bit_string(&rsa_pub)].concat());
let rdn = set(&seq(&[oid(&[2, 5, 4, 3]), utf8(subject_cn)].concat()));
let name = seq(&rdn);
let mut ext_list: Vec<u8> = Vec::new();
if let Some(u) = upn {
let other = ctx(
0,
&[oid(&[1, 3, 6, 1, 4, 1, 311, 20, 2, 3]), ctx(0, &utf8(u))].concat(),
);
let san = seq(&other); let san_ext = seq(&[oid(&[2, 5, 29, 17]), tlv(0x04, &san)].concat()); ext_list.extend(san_ext);
}
if let Some(sid) = target_sid {
let sid_value = build_sid_ext_value(sid);
let sid_ext = seq(&[oid(&[1, 3, 6, 1, 4, 1, 311, 25, 2]), tlv(0x04, &sid_value)].concat());
ext_list.extend(sid_ext);
}
let attributes = if ext_list.is_empty() {
ctx(0, &[])
} else {
let exts = seq(&ext_list); let attr = seq(&[oid(&[1, 2, 840, 113549, 1, 9, 14]), set(&exts)].concat()); ctx(0, &attr)
};
let cri = seq(&[int_bytes(vec![0]), name, spki, attributes].concat());
let sig = key.sign(Pkcs1v15Sign::new::<Sha256>(), &Sha256::digest(&cri))?;
let sig_alg = seq(&[oid(&[1, 2, 840, 113549, 1, 1, 11]), null()].concat());
let csr = seq(&[cri, sig_alg, bit_string(&sig)].concat());
let key_pem = key.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)?.to_string();
Ok(Csr { der: csr, key_pem })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn csr_is_wellformed_der_sequence() {
let c = build_csr("adhammer", Some("Administrator@corp.local")).unwrap();
assert_eq!(c.der[0], 0x30); assert!(c.der.len() > 400); assert!(c.key_pem.contains("BEGIN PRIVATE KEY"));
}
#[test]
fn dump_csr_for_external_validation() {
if let Ok(path) = std::env::var("ADHAMMER_CSR_OUT") {
let c = build_csr("adhammer", Some("Administrator@corp.local")).unwrap();
std::fs::write(&path, &c.der).unwrap();
}
}
#[test]
fn oid_encoding_upn() {
let o = oid(&[1, 3, 6, 1, 4, 1, 311, 20, 2, 3]);
assert_eq!(
&o[2..],
&[0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x14, 0x02, 0x03]
);
}
#[test]
fn sid_ext_value_matches_kb5014754_shape() {
let sid = "S-1-5-21-1-2-3-500";
let v = build_sid_ext_value(sid);
assert_eq!(v[0], 0x30, "outer tag must be SEQUENCE");
assert_eq!(
v[2], 0xA0,
"otherName must use IMPLICIT [0] constructed tag"
);
let sub_oid: &[u8] = &[
0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x19, 0x02, 0x01,
];
assert!(
v.windows(sub_oid.len()).any(|w| w == sub_oid),
"expected sub-OID .25.2.1 in encoded value"
);
assert!(
v.windows(sid.len()).any(|w| w == sid.as_bytes()),
"SID string must appear verbatim in extension value"
);
}
#[test]
fn csr_with_sid_ext_carries_both_extensions() {
let c = build_csr_with_sid_ext(
"adhammer",
Some("Administrator@corp.local"),
Some("S-1-5-21-1-2-3-500"),
)
.unwrap();
assert_eq!(c.der[0], 0x30);
let san_oid: &[u8] = &[0x06, 0x03, 0x55, 0x1d, 0x11];
assert!(
c.der.windows(san_oid.len()).any(|w| w == san_oid),
"SAN extension missing"
);
let sid_ext_oid: &[u8] = &[
0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x19, 0x02,
];
assert!(
c.der.windows(sid_ext_oid.len()).any(|w| w == sid_ext_oid),
"SID extension missing"
);
}
#[test]
fn csr_without_extensions_keeps_empty_attributes() {
let c = build_csr("adhammer-esc8", None).unwrap();
assert_eq!(c.der[0], 0x30);
assert!(c.der.len() > 300);
}
}