use std::sync::Once;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use openssl::hash::MessageDigest;
use openssl::pkcs12::Pkcs12;
use openssl::pkey::PKey;
use openssl::sign::Signer;
use sha1::{Digest, Sha1};
use fiscal_core::FiscalError;
use fiscal_core::types::{CertificateData, CertificateInfo};
fn ensure_legacy_provider() {
static INIT: Once = Once::new();
INIT.call_once(|| {
if let Ok(provider) = openssl::provider::Provider::try_load(None, "legacy", true) {
std::mem::forget(provider);
}
});
}
pub fn ensure_modern_pfx(pfx_buffer: &[u8], passphrase: &str) -> Result<Vec<u8>, FiscalError> {
ensure_legacy_provider();
let pkcs12 = Pkcs12::from_der(pfx_buffer)
.map_err(|e| FiscalError::Certificate(format!("Invalid PFX data: {e}")))?;
match pkcs12.parse2(passphrase) {
Ok(parsed) => {
re_export_pfx(&parsed, passphrase)
}
Err(e) => {
let msg = e.to_string();
if msg.contains("unsupported") || msg.contains("RC2") || msg.contains("mac") {
Err(FiscalError::Certificate(format!(
"Legacy PFX (RC2-40-CBC) detected but OpenSSL legacy provider \
could not handle it. Ensure OpenSSL 3.x with legacy provider \
support is available. Error: {e}"
)))
} else {
Err(FiscalError::Certificate(format!(
"Failed to parse PFX (wrong password?): {e}"
)))
}
}
}
}
fn re_export_pfx(
parsed: &openssl::pkcs12::ParsedPkcs12_2,
passphrase: &str,
) -> Result<Vec<u8>, FiscalError> {
let pkey = parsed
.pkey
.as_ref()
.ok_or_else(|| FiscalError::Certificate("PFX does not contain a private key".into()))?;
let cert = parsed
.cert
.as_ref()
.ok_or_else(|| FiscalError::Certificate("PFX does not contain a certificate".into()))?;
let mut builder = Pkcs12::builder();
if let Some(chain) = &parsed.ca {
let mut stack = openssl::stack::Stack::new()
.map_err(|e| FiscalError::Certificate(format!("Failed to create CA stack: {e}")))?;
for ca in chain {
stack
.push(ca.to_owned())
.map_err(|e| FiscalError::Certificate(format!("Failed to add CA to stack: {e}")))?;
}
builder.ca(stack);
}
let new_pfx = builder
.name("")
.pkey(pkey)
.cert(cert)
.build2(passphrase)
.map_err(|e| FiscalError::Certificate(format!("Failed to re-export PFX: {e}")))?;
new_pfx
.to_der()
.map_err(|e| FiscalError::Certificate(format!("Failed to serialize PFX: {e}")))
}
fn parse_pfx(
pfx_buffer: &[u8],
passphrase: &str,
) -> Result<openssl::pkcs12::ParsedPkcs12_2, FiscalError> {
let modern = ensure_modern_pfx(pfx_buffer, passphrase)?;
let pkcs12 = Pkcs12::from_der(&modern)
.map_err(|e| FiscalError::Certificate(format!("Invalid PFX data: {e}")))?;
pkcs12
.parse2(passphrase)
.map_err(|e| FiscalError::Certificate(format!("Failed to parse PFX: {e}")))
}
pub fn load_certificate(
pfx_buffer: &[u8],
passphrase: &str,
) -> Result<CertificateData, FiscalError> {
ensure_legacy_provider();
let parsed = parse_pfx(pfx_buffer, passphrase)?;
let pkey = parsed
.pkey
.ok_or_else(|| FiscalError::Certificate("PFX does not contain a private key".into()))?;
let cert = parsed
.cert
.ok_or_else(|| FiscalError::Certificate("PFX does not contain a certificate".into()))?;
let private_key_pem = String::from_utf8(
pkey.private_key_to_pem_pkcs8()
.map_err(|e| FiscalError::Certificate(format!("Failed to export private key: {e}")))?,
)
.map_err(|e| FiscalError::Certificate(format!("Private key PEM is not valid UTF-8: {e}")))?;
let certificate_pem = String::from_utf8(
cert.to_pem()
.map_err(|e| FiscalError::Certificate(format!("Failed to export certificate: {e}")))?,
)
.map_err(|e| FiscalError::Certificate(format!("Certificate PEM is not valid UTF-8: {e}")))?;
Ok(CertificateData::new(
private_key_pem,
certificate_pem,
pfx_buffer.to_vec(),
passphrase,
))
}
pub fn get_certificate_info(
pfx_buffer: &[u8],
passphrase: &str,
) -> Result<CertificateInfo, FiscalError> {
ensure_legacy_provider();
let parsed = parse_pfx(pfx_buffer, passphrase)?;
let cert = parsed
.cert
.ok_or_else(|| FiscalError::Certificate("PFX does not contain a certificate".into()))?;
let common_name = extract_cn_from_x509_name(cert.subject_name());
let issuer = extract_cn_from_x509_name(cert.issuer_name());
let valid_from = asn1_time_to_naive_date(cert.not_before())?;
let valid_until = asn1_time_to_naive_date(cert.not_after())?;
let serial_number = cert
.serial_number()
.to_bn()
.map_err(|e| FiscalError::Certificate(format!("Failed to read serial number: {e}")))?
.to_hex_str()
.map_err(|e| FiscalError::Certificate(format!("Failed to format serial number: {e}")))?
.to_string();
Ok(CertificateInfo::new(
common_name,
valid_from,
valid_until,
serial_number,
issuer,
))
}
pub fn sign_xml(xml: &str, private_key: &str, certificate: &str) -> Result<String, FiscalError> {
sign_xml_generic(xml, private_key, certificate, "infNFe", "NFe")
}
pub fn sign_event_xml(
xml: &str,
private_key: &str,
certificate: &str,
) -> Result<String, FiscalError> {
sign_xml_generic(xml, private_key, certificate, "infEvento", "evento")
}
pub fn sign_inutilizacao_xml(
xml: &str,
private_key: &str,
certificate: &str,
) -> Result<String, FiscalError> {
sign_xml_generic(xml, private_key, certificate, "infInut", "inutNFe")
}
fn sign_xml_generic(
xml: &str,
private_key_pem: &str,
certificate_pem: &str,
signed_tag: &str,
parent_tag: &str,
) -> Result<String, FiscalError> {
let id = extract_element_id(xml, signed_tag)?;
let signed_element = extract_element(xml, signed_tag).ok_or_else(|| {
FiscalError::Certificate(format!("<{signed_tag}> element not found in XML"))
})?;
let without_sig = remove_signature_element(&signed_element);
let with_inherited_ns = ensure_inherited_namespace(&without_sig, xml, signed_tag);
let canonical = canonicalize_xml(&with_inherited_ns);
let digest = {
let mut hasher = Sha1::new();
hasher.update(canonical.as_bytes());
BASE64.encode(hasher.finalize())
};
let signed_info = build_signed_info(&id, &digest);
let canonical_signed_info = signed_info.replacen(
"<SignedInfo>",
"<SignedInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">",
1,
);
let pkey = PKey::private_key_from_pem(private_key_pem.as_bytes())
.map_err(|e| FiscalError::Certificate(format!("Failed to parse private key: {e}")))?;
let mut signer = Signer::new(MessageDigest::sha1(), &pkey)
.map_err(|e| FiscalError::Certificate(format!("Failed to create signer: {e}")))?;
signer
.update(canonical_signed_info.as_bytes())
.map_err(|e| FiscalError::Certificate(format!("Failed to update signer: {e}")))?;
let signature_bytes = signer
.sign_to_vec()
.map_err(|e| FiscalError::Certificate(format!("RSA-SHA1 signing failed: {e}")))?;
let signature_value = BASE64.encode(&signature_bytes);
let cert_base64 = extract_cert_base64(certificate_pem);
let signature_xml = build_signature_element(&signed_info, &signature_value, &cert_base64);
let closing_tag = format!("</{parent_tag}>");
let result = if let Some(pos) = xml.rfind(&closing_tag) {
format!("{}{signature_xml}{}", &xml[..pos], &xml[pos..])
} else {
return Err(FiscalError::Certificate(format!(
"<{parent_tag}> closing tag not found in XML"
)));
};
Ok(result)
}
fn extract_element_id(xml: &str, tag_name: &str) -> Result<String, FiscalError> {
let pattern = format!("<{tag_name}");
let tag_start = xml.find(&pattern).ok_or_else(|| {
FiscalError::Certificate(format!(
"Could not find <{tag_name}> element with Id attribute in XML"
))
})?;
let rest = &xml[tag_start..];
let tag_end = rest
.find('>')
.ok_or_else(|| FiscalError::Certificate(format!("<{tag_name}> tag is malformed")))?;
let tag_content = &rest[..tag_end];
let id_prefix = "Id=\"";
let id_start = tag_content.find(id_prefix).ok_or_else(|| {
FiscalError::Certificate(format!(
"Could not find <{tag_name}> element with Id attribute in XML"
))
})?;
let id_value_start = id_start + id_prefix.len();
let id_value_end = tag_content[id_value_start..].find('"').ok_or_else(|| {
FiscalError::Certificate(format!("Malformed Id attribute in <{tag_name}>"))
})?;
Ok(tag_content[id_value_start..id_value_start + id_value_end].to_string())
}
fn ensure_inherited_namespace(element: &str, full_xml: &str, tag_name: &str) -> String {
let open_end = element.find('>').unwrap_or(element.len());
let open_tag = &element[..open_end];
if open_tag.contains("xmlns=") {
return element.to_string();
}
let tag_pos = full_xml.find(&format!("<{tag_name}")).unwrap_or(0);
let before = &full_xml[..tag_pos];
if let Some(ns_start) = before.rfind("xmlns=\"") {
let ns_val_start = ns_start + 7; if let Some(ns_val_end) = full_xml[ns_val_start..].find('"') {
let ns_value = &full_xml[ns_val_start..ns_val_start + ns_val_end];
let insert_pos = element
.find(|c: char| c.is_ascii_whitespace() || c == '>')
.unwrap_or(open_end);
return format!(
"{} xmlns=\"{ns_value}\"{}",
&element[..insert_pos],
&element[insert_pos..],
);
}
}
element.to_string()
}
fn extract_element(xml: &str, tag_name: &str) -> Option<String> {
let open_pattern = format!("<{tag_name}");
let close_pattern = format!("</{tag_name}>");
let start = xml.find(&open_pattern)?;
let end = xml.find(&close_pattern)? + close_pattern.len();
Some(xml[start..end].to_string())
}
fn remove_signature_element(xml: &str) -> String {
if let Some(sig_start) = xml.find("<Signature") {
if let Some(sig_end_tag) = xml[sig_start..].find("</Signature>") {
let sig_end = sig_start + sig_end_tag + "</Signature>".len();
return format!("{}{}", &xml[..sig_start], &xml[sig_end..]);
}
}
xml.to_string()
}
fn canonicalize_xml(xml: &str) -> String {
let mut input = xml;
if let Some(decl_start) = input.find("<?xml") {
if let Some(decl_end) = input[decl_start..].find("?>") {
input = input[decl_start + decl_end + 2..].trim_start();
}
}
let mut result = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '<' {
let mut tag = String::from('<');
for c in chars.by_ref() {
tag.push(c);
if c == '>' {
break;
}
}
if tag.starts_with("</") || tag.starts_with("<?") || tag.starts_with("<!") {
result.push_str(&tag);
continue;
}
let self_closing = tag.ends_with("/>");
let tag_content = if self_closing {
&tag[1..tag.len() - 2] } else {
&tag[1..tag.len() - 1] };
let (tag_name, attrs_str) = match tag_content.find(|c: char| c.is_ascii_whitespace()) {
Some(pos) => (&tag_content[..pos], tag_content[pos..].trim()),
None => (tag_content, ""),
};
if attrs_str.is_empty() {
if self_closing {
result.push('<');
result.push_str(tag_name);
result.push_str("></");
result.push_str(tag_name);
result.push('>');
} else {
result.push_str(&tag);
}
continue;
}
let attrs = parse_attributes(attrs_str);
let mut ns_attrs: Vec<(&str, &str)> = Vec::new();
let mut reg_attrs: Vec<(&str, &str)> = Vec::new();
for (name, value) in &attrs {
if *name == "xmlns" || name.starts_with("xmlns:") {
ns_attrs.push((name, value));
} else {
reg_attrs.push((name, value));
}
}
ns_attrs.sort_by(|a, b| match (a.0, b.0) {
("xmlns", _) => std::cmp::Ordering::Less,
(_, "xmlns") => std::cmp::Ordering::Greater,
_ => a.0.cmp(b.0),
});
reg_attrs.sort_by(|a, b| a.0.cmp(b.0));
result.push('<');
result.push_str(tag_name);
for (name, value) in ns_attrs.iter().chain(reg_attrs.iter()) {
result.push(' ');
result.push_str(name);
result.push_str("=\"");
result.push_str(value);
result.push('"');
}
if self_closing {
result.push_str("></");
result.push_str(tag_name);
result.push('>');
} else {
result.push('>');
}
} else {
result.push(ch);
}
}
result
}
fn parse_attributes(attrs_str: &str) -> Vec<(&str, &str)> {
let mut attrs = Vec::new();
let mut remaining = attrs_str.trim();
while !remaining.is_empty() {
let eq_pos = match remaining.find('=') {
Some(pos) => pos,
None => break,
};
let name = remaining[..eq_pos].trim();
remaining = remaining[eq_pos + 1..].trim();
let quote = match remaining.chars().next() {
Some(q @ ('"' | '\'')) => q,
_ => break,
};
remaining = &remaining[1..]; let end_pos = match remaining.find(quote) {
Some(pos) => pos,
None => break,
};
let value = &remaining[..end_pos];
remaining = remaining[end_pos + 1..].trim();
attrs.push((name, value));
}
attrs
}
fn build_signed_info(reference_id: &str, digest_value: &str) -> String {
let mut s = String::with_capacity(1024);
s.push_str("<SignedInfo>");
s.push_str("<CanonicalizationMethod Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"></CanonicalizationMethod>");
s.push_str("<SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"></SignatureMethod>");
s.push_str("<Reference URI=\"#");
s.push_str(reference_id);
s.push_str("\">");
s.push_str("<Transforms>");
s.push_str("<Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"></Transform>");
s.push_str(
"<Transform Algorithm=\"http://www.w3.org/TR/2001/REC-xml-c14n-20010315\"></Transform>",
);
s.push_str("</Transforms>");
s.push_str(
"<DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"></DigestMethod>",
);
s.push_str("<DigestValue>");
s.push_str(digest_value);
s.push_str("</DigestValue>");
s.push_str("</Reference>");
s.push_str("</SignedInfo>");
s
}
fn build_signature_element(signed_info: &str, signature_value: &str, cert_base64: &str) -> String {
let mut s = String::with_capacity(2048);
s.push_str("<Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\">");
s.push_str(signed_info);
s.push_str("<SignatureValue>");
s.push_str(signature_value);
s.push_str("</SignatureValue>");
s.push_str("<KeyInfo><X509Data><X509Certificate>");
s.push_str(cert_base64);
s.push_str("</X509Certificate></X509Data></KeyInfo>");
s.push_str("</Signature>");
s
}
fn extract_cert_base64(cert_pem: &str) -> String {
cert_pem
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.chars()
.filter(|c| !c.is_ascii_whitespace())
.collect()
}
fn extract_cn_from_x509_name(name: &openssl::x509::X509NameRef) -> String {
for entry in name.entries_by_nid(openssl::nid::Nid::COMMONNAME) {
if let Ok(s) = entry.data().as_utf8() {
return s.to_string();
}
}
format!("{:?}", name)
}
fn asn1_time_to_naive_date(
time: &openssl::asn1::Asn1TimeRef,
) -> Result<chrono::NaiveDate, FiscalError> {
let epoch = openssl::asn1::Asn1Time::from_unix(0)
.map_err(|e| FiscalError::Certificate(format!("ASN1 epoch creation failed: {e}")))?;
let diff = epoch
.diff(time)
.map_err(|e| FiscalError::Certificate(format!("ASN1 time diff failed: {e}")))?;
let days = diff.days as i64;
let secs = diff.secs as i64;
let total_secs = days * 86400 + secs;
let dt = chrono::DateTime::from_timestamp(total_secs, 0)
.ok_or_else(|| FiscalError::Certificate("Invalid timestamp from ASN1 time".into()))?;
Ok(dt.date_naive())
}
#[cfg(test)]
mod tests {
use super::*;
fn test_pfx_cnpj() -> Vec<u8> {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../..",
"/tests/fixtures/certs/novo_cert_cnpj_06157250000116_senha_minhasenha.pfx"
);
std::fs::read(path).expect("test PFX not found")
}
fn test_pfx_cpf() -> Vec<u8> {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../..",
"/tests/fixtures/certs/novo_cert_cpf_90483926086_minhasenha.pfx"
);
std::fs::read(path).expect("test PFX not found")
}
const PASSWORD: &str = "minhasenha";
#[test]
fn ensure_modern_pfx_valid_cert() {
let pfx = test_pfx_cnpj();
let result = ensure_modern_pfx(&pfx, PASSWORD);
assert!(result.is_ok());
assert!(!result.unwrap().is_empty());
}
#[test]
fn ensure_modern_pfx_wrong_password() {
let pfx = test_pfx_cnpj();
let result = ensure_modern_pfx(&pfx, "wrongpassword");
assert!(result.is_err());
}
#[test]
fn ensure_modern_pfx_invalid_data() {
let result = ensure_modern_pfx(b"not a pfx", PASSWORD);
assert!(result.is_err());
}
#[test]
fn load_certificate_cnpj() {
let pfx = test_pfx_cnpj();
let cert_data = load_certificate(&pfx, PASSWORD).expect("should load");
assert!(!cert_data.private_key.is_empty());
assert!(!cert_data.certificate.is_empty());
assert!(cert_data.private_key.contains("PRIVATE KEY"));
assert!(cert_data.certificate.contains("CERTIFICATE"));
}
#[test]
fn load_certificate_cpf() {
let pfx = test_pfx_cpf();
let cert_data = load_certificate(&pfx, PASSWORD).expect("should load");
assert!(!cert_data.private_key.is_empty());
assert!(!cert_data.certificate.is_empty());
}
#[test]
fn load_certificate_wrong_password() {
let pfx = test_pfx_cnpj();
let result = load_certificate(&pfx, "wrong");
assert!(result.is_err());
}
#[test]
fn load_certificate_invalid_pfx() {
let result = load_certificate(b"invalid data", PASSWORD);
assert!(result.is_err());
}
#[test]
fn get_certificate_info_cnpj() {
let pfx = test_pfx_cnpj();
let info = get_certificate_info(&pfx, PASSWORD).expect("should get info");
assert!(!info.common_name.is_empty());
assert!(!info.serial_number.is_empty());
}
#[test]
fn get_certificate_info_cpf() {
let pfx = test_pfx_cpf();
let info = get_certificate_info(&pfx, PASSWORD).expect("should get info");
assert!(!info.common_name.is_empty());
}
#[test]
fn get_certificate_info_wrong_password() {
let pfx = test_pfx_cnpj();
let result = get_certificate_info(&pfx, "wrong");
assert!(result.is_err());
}
#[test]
fn sign_xml_basic() {
let pfx = test_pfx_cnpj();
let cert_data = load_certificate(&pfx, PASSWORD).unwrap();
let xml = concat!(
r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe">"#,
r#"<infNFe versao="4.00" Id="NFe41260304123456000190550010000001231123456780">"#,
"<ide><cUF>41</cUF></ide>",
"</infNFe></NFe>"
);
let signed =
sign_xml(xml, &cert_data.private_key, &cert_data.certificate).expect("should sign");
assert!(signed.contains("<Signature"));
assert!(signed.contains("<SignatureValue>"));
assert!(signed.contains("<X509Certificate>"));
assert!(signed.contains("<DigestValue>"));
}
#[test]
fn sign_xml_missing_tag() {
let pfx = test_pfx_cnpj();
let cert_data = load_certificate(&pfx, PASSWORD).unwrap();
let xml = "<other>content</other>";
let result = sign_xml(xml, &cert_data.private_key, &cert_data.certificate);
assert!(result.is_err());
}
#[test]
fn sign_xml_no_id_attribute() {
let pfx = test_pfx_cnpj();
let cert_data = load_certificate(&pfx, PASSWORD).unwrap();
let xml = "<NFe><infNFe><data/></infNFe></NFe>";
let result = sign_xml(xml, &cert_data.private_key, &cert_data.certificate);
assert!(result.is_err());
}
#[test]
fn sign_event_xml_basic() {
let pfx = test_pfx_cnpj();
let cert_data = load_certificate(&pfx, PASSWORD).unwrap();
let xml = concat!(
r#"<evento xmlns="http://www.portalfiscal.inf.br/nfe" versao="1.00">"#,
r#"<infEvento Id="ID1101114126030412345600019055001000000012312345678001">"#,
"<cOrgao>41</cOrgao>",
"</infEvento></evento>"
);
let signed = sign_event_xml(xml, &cert_data.private_key, &cert_data.certificate)
.expect("should sign");
assert!(signed.contains("<Signature"));
assert!(signed.contains("<SignatureValue>"));
}
#[test]
fn sign_event_xml_missing_inf_evento() {
let pfx = test_pfx_cnpj();
let cert_data = load_certificate(&pfx, PASSWORD).unwrap();
let xml = "<evento><other/></evento>";
let result = sign_event_xml(xml, &cert_data.private_key, &cert_data.certificate);
assert!(result.is_err());
}
#[test]
fn extract_element_id_success() {
let xml = r#"<infNFe versao="4.00" Id="NFe41260312345678000199550010000000011123456780"><data/></infNFe>"#;
let id = extract_element_id(xml, "infNFe").unwrap();
assert_eq!(id, "NFe41260312345678000199550010000000011123456780");
}
#[test]
fn extract_element_id_no_tag() {
let result = extract_element_id("<other/>", "infNFe");
assert!(result.is_err());
}
#[test]
fn extract_element_id_no_id_attr() {
let xml = "<infNFe versao=\"4.00\"><data/></infNFe>";
let result = extract_element_id(xml, "infNFe");
assert!(result.is_err());
}
#[test]
fn canonicalize_xml_removes_declaration() {
let xml = "<?xml version=\"1.0\"?><root><a/></root>";
let canonical = canonicalize_xml(xml);
assert!(!canonical.contains("<?xml"));
assert!(canonical.contains("<a></a>"));
}
#[test]
fn canonicalize_xml_sorts_attributes() {
let xml = r#"<root b="2" a="1"><child/></root>"#;
let canonical = canonicalize_xml(xml);
assert!(canonical.contains(r#"<root a="1" b="2">"#));
}
#[test]
fn canonicalize_xml_ns_first() {
let xml = r#"<root b="2" xmlns="http://example.com" a="1"><child/></root>"#;
let canonical = canonicalize_xml(xml);
assert!(canonical.starts_with(r#"<root xmlns="http://example.com""#));
}
#[test]
fn remove_signature_element_present() {
let xml = "<root><data/><Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><SignedInfo/></Signature></root>";
let result = remove_signature_element(xml);
assert!(!result.contains("<Signature"));
assert!(result.contains("<root>"));
assert!(result.contains("<data/>"));
}
#[test]
fn remove_signature_element_absent() {
let xml = "<root><data/></root>";
let result = remove_signature_element(xml);
assert_eq!(result, xml);
}
#[test]
fn extract_cert_base64_strips_headers() {
let pem = "-----BEGIN CERTIFICATE-----\nTWFu\n-----END CERTIFICATE-----\n";
let b64 = extract_cert_base64(pem);
assert_eq!(b64, "TWFu");
}
#[test]
fn ensure_inherited_namespace_already_has_xmlns() {
let element = r#"<infNFe xmlns="http://www.portalfiscal.inf.br/nfe" Id="X">data</infNFe>"#;
let full_xml = element;
let result = ensure_inherited_namespace(element, full_xml, "infNFe");
assert_eq!(result, element);
}
#[test]
fn ensure_inherited_namespace_inherits_from_parent() {
let element = r#"<infNFe Id="X">data</infNFe>"#;
let full_xml =
r#"<NFe xmlns="http://www.portalfiscal.inf.br/nfe"><infNFe Id="X">data</infNFe></NFe>"#;
let result = ensure_inherited_namespace(element, full_xml, "infNFe");
assert!(result.contains("xmlns=\"http://www.portalfiscal.inf.br/nfe\""));
}
#[test]
fn parse_attributes_basic() {
let attrs = parse_attributes(r#"a="1" b="2""#);
assert_eq!(attrs.len(), 2);
assert_eq!(attrs[0], ("a", "1"));
assert_eq!(attrs[1], ("b", "2"));
}
#[test]
fn parse_attributes_single_quotes() {
let attrs = parse_attributes("a='1'");
assert_eq!(attrs.len(), 1);
assert_eq!(attrs[0], ("a", "1"));
}
#[test]
fn parse_attributes_empty() {
let attrs = parse_attributes("");
assert!(attrs.is_empty());
}
#[test]
fn sign_inutilizacao_xml_basic() {
let pfx = test_pfx_cnpj();
let cert_data = load_certificate(&pfx, PASSWORD).unwrap();
let xml = concat!(
r#"<inutNFe xmlns="http://www.portalfiscal.inf.br/nfe" versao="4.00">"#,
r#"<infInut Id="ID41260304123456000190550010000001231000000010">"#,
"<tpAmb>2</tpAmb>",
"</infInut></inutNFe>"
);
let signed = sign_inutilizacao_xml(xml, &cert_data.private_key, &cert_data.certificate)
.expect("should sign inutilizacao");
assert!(signed.contains("<Signature"));
assert!(signed.contains("<SignatureValue>"));
assert!(signed.contains("<X509Certificate>"));
}
#[test]
fn sign_inutilizacao_xml_missing_inf_inut() {
let pfx = test_pfx_cnpj();
let cert_data = load_certificate(&pfx, PASSWORD).unwrap();
let xml = "<inutNFe><other/></inutNFe>";
let result = sign_inutilizacao_xml(xml, &cert_data.private_key, &cert_data.certificate);
assert!(result.is_err());
}
#[test]
fn sign_xml_generic_missing_parent_closing_tag() {
let pfx = test_pfx_cnpj();
let cert_data = load_certificate(&pfx, PASSWORD).unwrap();
let xml =
r#"<NFe><infNFe Id="NFe41260304123456000190550010000001231123456780"><data/></infNFe>"#;
let result = sign_xml(xml, &cert_data.private_key, &cert_data.certificate);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("closing tag not found"),
"Expected 'closing tag not found' error, got: {err_msg}"
);
}
#[test]
fn extract_element_id_malformed_id() {
let xml = r#"<infNFe Id="NFe41260312345678></infNFe>"#;
let result = extract_element_id(xml, "infNFe");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Malformed Id"),
"Expected 'Malformed Id' error, got: {err_msg}"
);
}
#[test]
fn ensure_inherited_namespace_no_xmlns_anywhere() {
let element = r#"<infNFe Id="X">data</infNFe>"#;
let full_xml = r#"<NFe><infNFe Id="X">data</infNFe></NFe>"#;
let result = ensure_inherited_namespace(element, full_xml, "infNFe");
assert_eq!(result, element);
}
#[test]
fn canonicalize_xml_sorts_multiple_ns_prefixes() {
let xml = r#"<root xmlns:z="http://z.example.com" xmlns="http://default.example.com" xmlns:a="http://a.example.com"><child/></root>"#;
let canonical = canonicalize_xml(xml);
assert!(canonical.contains(
r#"<root xmlns="http://default.example.com" xmlns:a="http://a.example.com" xmlns:z="http://z.example.com">"#
));
}
#[test]
fn canonicalize_xml_self_closing_with_attrs() {
let xml = r#"<root><item b="2" a="1"/></root>"#;
let canonical = canonicalize_xml(xml);
assert!(canonical.contains(r#"<item a="1" b="2"></item>"#));
}
}