diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 3e173be1825..64d933590a8 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -1,7 +1,7 @@ name: CIFuzz on: - pull_request: + push: branches: [master] permissions: diff --git a/doc/scapy/layers/kerberos.rst b/doc/scapy/layers/kerberos.rst index 168e2d7ecab..2ab19f5db83 100644 --- a/doc/scapy/layers/kerberos.rst +++ b/doc/scapy/layers/kerberos.rst @@ -41,11 +41,11 @@ This section tries to give many usage examples, but isn't exhaustive. For more d >>> t.show() Tickets: 0. Administrator@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL - Start time End time Renew until Auth time + Start time End time Renew until Auth time 31/08/23 11:38:34 31/08/23 21:38:34 31/08/23 21:38:35 31/08/23 01:38:34 1. Administrator@DOMAIN.LOCAL -> host/dc1.domain.local@DOMAIN.LOCAL - Start time End time Renew until Auth time + Start time End time Renew until Auth time 31/08/23 11:39:07 31/08/23 21:38:34 31/08/23 21:38:35 31/08/23 01:38:34 @@ -68,12 +68,41 @@ This section tries to give many usage examples, but isn't exhaustive. For more d >>> # Using the AES-256-SHA1-96 Kerberos Key >>> t.request_tgt("Administrator@domain.local", key=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, bytes.fromhex("63a2577d8bf6abeba0847cded36b9aed202c23750eb9c56b6155be1cc946bb1d"))) +- **Request a TGT using PKINIT**: + +.. code:: pycon + + >>> from scapy.libs.rfc3961 import EncryptionType + >>> load_module("ticketer") + >>> t = Ticketer() + >>> # If P12: + >>> t.request_tgt("Administrator@DOMAIN.LOCAL", p12="admin.pfx", ca="ca.pem") + >>> # One could also have used a different cert and key file: + >>> t.request_tgt("Administrator@DOMAIN.LOCAL", x509="admin.cert", x509key="admin.key", ca="ca.pem") + +- **Request a user TGT with Kerberos armoring (FAST)** + +The ``armor_with`` keyword allows to select a ticket to armor the request with. + +.. code:: pycon + + >>> load_module("ticketer") + >>> t = Ticketer() + >>> t.request_tgt("Machine01$@DOMAIN.LOCAL", key=Key(EncryptionType.RC4_HMAC, bytes.fromhex("2b576acbe6bcfda7294d6bd18041b8fe"))) + >>> t.show() + Tickets: + 0. Machine01$@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + Start time End time Renew until Auth time + 31/08/23 11:38:34 31/08/23 21:38:34 31/08/23 21:38:35 31/08/23 01:38:34 + >>> t.request_tgt("Administrator@domain.local", armor_with=0) # Armor with ticket n°0 + - **Renew a TGT or ST**: .. code:: >>> t.renew(0) # renew TGT >>> t.renew(1) # renew ST. Works only with 'host/' SPNs + >>> t.renew(1, armor_with=0) # renew something with armoring - **Import tickets from a ccache**: diff --git a/scapy/asn1/mib.py b/scapy/asn1/mib.py index 029f8281225..7bb30811e00 100644 --- a/scapy/asn1/mib.py +++ b/scapy/asn1/mib.py @@ -284,16 +284,25 @@ def load_mib(filenames): "1.3.101.113": "Ed448", } +# pkcs3 # + +pkcs3_oids = { + "1.2.840.113549.1.3": "pkcs-3", + "1.2.840.113549.1.3.1": "dhKeyAgreement", +} + # pkcs7 # pkcs7_oids = { + "1.2.840.113549.1.7": "pkcs-7", "1.2.840.113549.1.7.2": "id-signedData", + "1.2.840.113549.1.7.3": "id-envelopedData", } # pkcs9 # pkcs9_oids = { - "1.2.840.113549.1.9": "pkcs9", + "1.2.840.113549.1.9": "pkcs-9", "1.2.840.113549.1.9.0": "modules", "1.2.840.113549.1.9.1": "emailAddress", "1.2.840.113549.1.9.2": "unstructuredName", @@ -320,6 +329,13 @@ def load_mib(filenames): "1.2.840.113549.1.9.52": "id-aa-CMSAlgorithmProtection" } +# enc algs # + +encAlgs_oids = { + "1.2.840.113549.3.4": "rc4", + "1.2.840.113549.3.7": "des-ede3-cbc", +} + # x509 # attributeType_oids = { @@ -492,10 +508,21 @@ def load_mib(filenames): "2.5.29.67": "id-ce-allowedAttAss", "2.5.29.68": "id-ce-attributeMappings", "2.5.29.69": "id-ce-holderNameConstraints", - # [MS-WCCE] - "1.3.6.1.4.1.311.2.1.14": "CERT_EXTENSIONS", - "1.3.6.1.4.1.311.10.3.4": "szOID_EFS_CRYPTO", + # [MS-WCCE] + wincrypt.h + "1.3.6.1.4.1.311.2.1.14": "OID_CERT_EXTENSIONS", + "1.3.6.1.4.1.311.10.3.4": "OID_EFS_CRYPTO", + "1.3.6.1.4.1.311.13.2.1": "OID_ENROLLMENT_NAME_VALUE_PAIR", + "1.3.6.1.4.1.311.13.2.2": "OID_ENROLLMENT_CSP_PROVIDER", + "1.3.6.1.4.1.311.13.2.3": "OID_OS_VERSION", + "1.3.6.1.4.1.311.10.10.1": "OID_CMC_ADD_ATTRIBUTES", "1.3.6.1.4.1.311.20.2": "ENROLL_CERTTYPE", + "1.3.6.1.4.1.311.21.10": "OID_APPLICATION_CERT_POLICIES", + "1.3.6.1.4.1.311.21.20": "OID_REQUEST_CLIENT_INFO", + "1.3.6.1.4.1.311.21.23": "OID_ENROLL_EK_INFO", + "1.3.6.1.4.1.311.21.24": "OID_ENROLL_ATTESTATION_STATEMENT", + "1.3.6.1.4.1.311.21.25": "OID_ENROLL_KSP_NAME", + "1.3.6.1.4.1.311.21.39": "OID_ENROLL_AIK_INFO", + "1.3.6.1.4.1.311.21.7": "OID_CERTIFICATE_TEMPLATE", "1.3.6.1.4.1.311.25.1": "NTDS_REPLICATION", "1.3.6.1.4.1.311.25.2": "NTDS_CA_SECURITY_EXT", "1.3.6.1.4.1.311.25.2.1": "NTDS_OBJECTSID", @@ -551,6 +578,15 @@ def load_mib(filenames): "1.3.6.1.5.5.7.3.22": "secureShellServer" } +certPkixCmc_oids = { + "1.3.6.1.5.5.7.7.8": "id-cmc-addExtensions", +} + +certPkixCct_oids = { + "1.3.6.1.5.5.7.12.2": "id-cct-PKIData", + "1.3.6.1.5.5.7.12.3": "id-cct-PKIResponse", +} + certPkixAd_oids = { "1.3.6.1.5.5.7.48.1": "ocsp", "1.3.6.1.5.5.7.48.2": "caIssuers", @@ -563,6 +599,11 @@ def load_mib(filenames): "1.3.6.1.5.5.7.48.1.1": "basic-response" } +certIpsec_oids = { + "1.3.6.1.5.5.8.2.1": "iKEEnd", + "1.3.6.1.5.5.8.2.2": "iKEIntermediate", +} + certTransp_oids = { '1.3.6.1.4.1.11129.2.4.2': "SignedCertificateTimestampList", } @@ -724,16 +765,21 @@ def load_mib(filenames): secsig_oids, nist_oids, thawte_oids, + pkcs3_oids, pkcs7_oids, pkcs9_oids, + encAlgs_oids, attributeType_oids, certificateExtension_oids, certExt_oids, certPkixAd_oids, certPkixKp_oids, + certPkixCmc_oids, + certPkixCct_oids, certPkixPe_oids, certPkixQt_oids, certPolicy_oids, + certIpsec_oids, certTransp_oids, evPolicy_oids, x962KeyType_oids, diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index f2d8613af37..9a5284bddfc 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -31,7 +31,6 @@ BER_tagging_enc, ) from scapy.base_classes import BasePacket -from scapy.compat import raw from scapy.volatile import ( GeneralizedTime, RandChoice, @@ -599,7 +598,7 @@ def build(self, pkt): elif val is None: s = b"" else: - s = b"".join(raw(i) for i in val) + s = b"".join(bytes(i) for i in val) return self.i2m(pkt, s) def i2repr(self, pkt, x): @@ -642,6 +641,9 @@ class ASN1F_TIME_TICKS(ASN1F_INTEGER): ############################# class ASN1F_optional(ASN1F_element): + """ + ASN.1 field that is optional. + """ def __init__(self, field): # type: (ASN1F_field[Any, Any]) -> None field.flexible_tag = False @@ -682,6 +684,20 @@ def i2repr(self, pkt, x): return self._field.i2repr(pkt, x) +class ASN1F_omit(ASN1F_field[None, None]): + """ + ASN.1 field that is not specified. This is simply omitted on the network. + This is different from ASN1F_NULL which has a network representation. + """ + def m2i(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[None, bytes] + return None, s + + def i2m(self, pkt, x): + # type: (ASN1_Packet, Optional[bytes]) -> bytes + return b"" + + _CHOICE_T = Union['ASN1_Packet', Type[ASN1F_field[Any, Any]], 'ASN1F_PACKET'] @@ -769,7 +785,7 @@ def i2m(self, pkt, x): if x is None: s = b"" else: - s = raw(x) + s = bytes(x) if hash(type(x)) in self.pktchoices: imp, exp = self.pktchoices[hash(type(x))] s = BER_tagging_enc(s, @@ -852,11 +868,11 @@ def i2m(self, s = x elif isinstance(x, ASN1_Object): if x.val: - s = raw(x.val) + s = bytes(x.val) else: s = b"" else: - s = raw(x) + s = bytes(x) if not hasattr(x, "ASN1_root"): # A normal Packet (!= ASN1) return s @@ -897,7 +913,7 @@ def __init__(self, self.cls = cls super(ASN1F_BIT_STRING_ENCAPS, self).__init__( # type: ignore name, - default and raw(default), + default and bytes(default), context=context, implicit_tag=implicit_tag, explicit_tag=explicit_tag diff --git a/scapy/layers/dcerpc.py b/scapy/layers/dcerpc.py index 28dfa6f97a0..7174e59993b 100644 --- a/scapy/layers/dcerpc.py +++ b/scapy/layers/dcerpc.py @@ -451,6 +451,14 @@ class RPC_C_AUTHN_LEVEL(IntEnum): DCE_C_AUTHN_LEVEL = RPC_C_AUTHN_LEVEL # C706 name +class RPC_C_IMP_LEVEL(IntEnum): + DEFAULT = 0x0 + ANONYMOUS = 0x1 + IDENTIFY = 0x2 + IMPERSONATE = 0x3 + DELEGATE = 0x4 + + # C706 sect 13.2.6.1 @@ -2766,9 +2774,9 @@ def __init__(self, *args, **kwargs): self.ssp = kwargs.pop("ssp", None) self.sspcontext = kwargs.pop("sspcontext", None) self.auth_level = kwargs.pop("auth_level", None) - self.auth_context_id = kwargs.pop("auth_context_id", 0) self.sent_cont_ids = [] self.cont_id = 0 # Currently selected context + self.auth_context_id = 0 # Currently selected authentication context self.map_callid_opnum = {} self.frags = collections.defaultdict(lambda: b"") self.sniffsspcontexts = {} # Unfinished contexts for passive @@ -3283,7 +3291,6 @@ def __init__(self, *args, **kwargs): self.session = DceRpcSession( ssp=kwargs.pop("ssp", None), auth_level=kwargs.pop("auth_level", None), - auth_context_id=kwargs.pop("auth_context_id", None), support_header_signing=kwargs.pop("support_header_signing", True), ) super(DceRpcSocket, self).__init__(*args, **kwargs) diff --git a/scapy/layers/gssapi.py b/scapy/layers/gssapi.py index 547e09a4734..d14f2360f43 100644 --- a/scapy/layers/gssapi.py +++ b/scapy/layers/gssapi.py @@ -34,7 +34,7 @@ ASN1_Class_UNIVERSAL, ASN1_Codecs, ) -from scapy.asn1.ber import BERcodec_SEQUENCE +from scapy.asn1.ber import BERcodec_SEQUENCE, BER_id_dec from scapy.asn1.mib import conf # loads conf.mib from scapy.asn1fields import ( ASN1F_OID, @@ -104,19 +104,25 @@ class GSSAPI_BLOB(ASN1_Packet): @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): if _pkt and len(_pkt) >= 1: - if ord(_pkt[:1]) & 0xA0 >= 0xA0: + if _pkt[0] & 0xA0 >= 0xA0: from scapy.layers.spnego import SPNEGO_negToken # XXX: sometimes the token is raw, we should look from # the session what to use here. For now: hardcode SPNEGO # (THIS IS A VERY STRONG ASSUMPTION) return SPNEGO_negToken - if _pkt[:7] == b"NTLMSSP": + elif _pkt[:7] == b"NTLMSSP": from scapy.layers.ntlm import NTLM_Header # XXX: if no mechTypes are provided during SPNEGO exchange, # Windows falls back to a plain NTLM_Header. return NTLM_Header.dispatch_hook(_pkt=_pkt, *args, **kargs) + elif BER_id_dec(_pkt)[0] & 0x7F > 0x60: + from scapy.layers.kerberos import Kerberos + + # XXX: Heuristic to detect raw Kerberos packets, when Windows + # fallsback or when the parent data hasn't got any mechtype specified. + return Kerberos return cls @@ -454,7 +460,7 @@ class STATE(IntEnum): def GSS_Init_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, target_name: Optional[str] = None, req_flags: Optional[GSS_C_FLAGS] = None, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, @@ -468,7 +474,7 @@ def GSS_Init_sec_context( def GSS_Accept_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): @@ -477,10 +483,21 @@ def GSS_Accept_sec_context( """ raise NotImplementedError + @abc.abstractmethod + def GSS_Inquire_names_for_mech(self) -> List[str]: + """ + Get the available OIDs for this mech, in order of preference. + """ + raise NotImplementedError + # Passive @abc.abstractmethod - def GSS_Passive(self, Context: CONTEXT, token=None): + def GSS_Passive( + self, + Context: CONTEXT, + input_token=None, + ): """ GSS_Passive: client/server call for the SSP in passive mode """ @@ -591,6 +608,9 @@ def GSS_GetMIC( message: bytes, qop_req: int = GSS_C_QOP_DEFAULT, ): + """ + See GSS_GetMICEx + """ return self.GSS_GetMICEx( Context, [ @@ -609,7 +629,10 @@ def GSS_VerifyMIC( Context: CONTEXT, message: bytes, signature, - ): + ) -> None: + """ + See GSS_VerifyMICEx + """ self.GSS_VerifyMICEx( Context, [ @@ -630,6 +653,9 @@ def GSS_Wrap( conf_req_flag: bool, qop_req: int = GSS_C_QOP_DEFAULT, ): + """ + See GSS_WrapEx + """ _msgs, signature = self.GSS_WrapEx( Context, [ @@ -647,7 +673,14 @@ def GSS_Wrap( # sect 2.3.4 - def GSS_Unwrap(self, Context: CONTEXT, signature): + def GSS_Unwrap( + self, + Context: CONTEXT, + signature, + ): + """ + See GSS_UnwrapEx + """ data = b"" if signature.payload: # signature has a payload that is the data. Let's get that payload @@ -679,19 +712,19 @@ def NegTokenInit2(self): """ return None, None - def canMechListMIC(self, Context: CONTEXT): + def SupportsMechListMIC(self): """ - Returns whether or not mechListMIC can be computed + Returns whether mechListMIC is supported or not """ - return False + return True - def getMechListMIC(self, Context, input): + def GetMechListMIC(self, Context, input): """ Compute mechListMIC """ - return bytes(self.GSS_GetMIC(Context, input)) + return self.GSS_GetMIC(Context, input) - def verifyMechListMIC(self, Context, otherMIC, input): + def VerifyMechListMIC(self, Context, otherMIC, input): """ Verify mechListMIC """ diff --git a/scapy/layers/http.py b/scapy/layers/http.py index 016337738fc..26fc3727eb0 100644 --- a/scapy/layers/http.py +++ b/scapy/layers/http.py @@ -763,6 +763,7 @@ class HTTP_Client(object): :param ssl: whether to use HTTPS or not :param ssp: the SSP object to use for binding :param no_check_certificate: with SSL, do not check the certificate + :param no_chan_bindings: force disable sending the channel bindings """ def __init__( @@ -772,6 +773,7 @@ def __init__( sslcontext=None, ssp=None, no_check_certificate=False, + no_chan_bindings=False, ): self.sock = None self._sockinfo = None @@ -781,6 +783,7 @@ def __init__( self.ssp = ssp self.sspcontext = None self.no_check_certificate = no_check_certificate + self.no_chan_bindings = no_chan_bindings self.chan_bindings = GSS_C_NO_CHANNEL_BINDINGS def _connect_or_reuse(self, host, port=None, tls=False, timeout=5): @@ -823,7 +826,7 @@ def _connect_or_reuse(self, host, port=None, tls=False, timeout=5): else: context = self.sslcontext sock = context.wrap_socket(sock, server_hostname=host) - if self.ssp: + if self.ssp and not self.no_chan_bindings: # Compute the channel binding token (CBT) self.chan_bindings = GssChannelBindings.fromssl( ChannelBindingType.TLS_SERVER_END_POINT, @@ -941,7 +944,7 @@ def request( # SPNEGO / Kerberos / NTLM self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, - ssp_blob, + input_token=ssp_blob, target_name="http/" + host, req_flags=0, chan_bindings=self.chan_bindings, diff --git a/scapy/layers/kerberos.py b/scapy/layers/kerberos.py index c8a3320d6e7..a3125382ed5 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -63,11 +63,12 @@ ASN1_BIT_STRING, ASN1_BOOLEAN, ASN1_Class, + ASN1_Codecs, ASN1_GENERAL_STRING, ASN1_GENERALIZED_TIME, ASN1_INTEGER, + ASN1_OID, ASN1_STRING, - ASN1_Codecs, ) from scapy.asn1fields import ( ASN1F_BIT_STRING_ENCAPS, @@ -135,6 +136,7 @@ GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED, GSS_S_DEFECTIVE_TOKEN, + GSS_S_DEFECTIVE_CREDENTIAL, GSS_S_FAILURE, GSS_S_FLAGS, GssChannelBindings, @@ -145,7 +147,20 @@ from scapy.layers.inet import TCP, UDP from scapy.layers.smb import _NV_VERSION from scapy.layers.smb2 import STATUS_ERREF -from scapy.layers.tls.cert import Cert, PrivKey +from scapy.layers.tls.cert import ( + Cert, + CertList, + CertTree, + CMS_Engine, + PrivKey, +) +from scapy.layers.tls.crypto.hash import ( + Hash_SHA, + Hash_SHA256, + Hash_SHA384, + Hash_SHA512, +) +from scapy.layers.tls.crypto.groups import _ffdh_groups from scapy.layers.x509 import ( _CMS_ENCAPSULATED, CMS_ContentInfo, @@ -154,17 +169,37 @@ X509_AlgorithmIdentifier, X509_DirectoryName, X509_SubjectPublicKeyInfo, + DomainParameters, ) # Redirect exports from RFC3961 try: from scapy.libs.rfc3961 import * # noqa: F401,F403 + from scapy.libs.rfc3961 import ( + _rfc1964pad, + ChecksumType, + Cipher, + decrepit_algorithms, + EncryptionType, + Hmac_MD5, + Key, + KRB_FX_CF2, + octetstring2key, + ) except ImportError: pass + +# Crypto imports +if conf.crypto_valid: + from cryptography.hazmat.primitives.serialization import pkcs12 + from cryptography.hazmat.primitives.asymmetric import dh + # Typing imports from typing import ( + List, Optional, + Union, ) @@ -356,6 +391,9 @@ def get_usage(self): elif isinstance(self.underlayer, KRB_AS_REP): # AS-REP encrypted part return 3, EncASRepPart + elif isinstance(self.underlayer, KRB_KDC_REQ_BODY): + # KDC-REQ enc-authorization-data + return 4, AuthorizationData elif isinstance(self.underlayer, KRB_AP_REQ) and isinstance( self.underlayer.underlayer, PADATA ): @@ -450,8 +488,6 @@ class EncryptionKey(ASN1_Packet): ) def toKey(self): - from scapy.libs.rfc3961 import Key - return Key( etype=self.keytype.val, key=self.keyvalue.val, @@ -519,7 +555,7 @@ def get_usage(self): def verify(self, key, text, key_usage_number=None): """ - Decrypt and return the data contained in cipher. + Verify a signature of text using a key. :param key: the key to use to check the checksum :param text: the bytes to verify @@ -532,7 +568,7 @@ def verify(self, key, text, key_usage_number=None): def make(self, key, text, key_usage_number=None, cksumtype=None): """ - Encrypt text and set it into cipher. + Make a signature. :param key: the key to use to make the checksum :param text: the bytes to make a checksum of @@ -950,9 +986,10 @@ class KERB_AD_RESTRICTION_ENTRY(ASN1_Packet): class KERB_AUTH_DATA_AP_OPTIONS(Packet): name = "KERB-AUTH-DATA-AP-OPTIONS" fields_desc = [ - LEIntEnumField( + FlagsField( "apOptions", 0x4000, + -32, { 0x4000: "KERB_AP_OPTIONS_CBT", 0x8000: "KERB_AP_OPTIONS_UNVERIFIED_TARGET_NAME", @@ -1248,7 +1285,7 @@ class PA_PK_AS_REQ(ASN1_Packet): ASN1F_optional( ASN1F_SEQUENCE_OF( "trustedCertifiers", - [ExternalPrincipalIdentifier()], + None, ExternalPrincipalIdentifier, explicit_tag=0xA1, ), @@ -1277,11 +1314,59 @@ class PAChecksum2(ASN1_Packet): ), ) + def verify(self, text): + """ + Verify a checksum of text. + + :param text: the bytes to verify + """ + # [MS-PKCA] 2.2.3 - PAChecksum2 + + # Only some OIDs are supported. Dumb but readable code. + oid = self.algorithmIdentifier.algorithm.val + if oid == "1.3.14.3.2.26": + hashcls = Hash_SHA + elif oid == "2.16.840.1.101.3.4.2.1": + hashcls = Hash_SHA256 + elif oid == "2.16.840.1.101.3.4.2.2": + hashcls = Hash_SHA384 + elif oid == "2.16.840.1.101.3.4.2.3": + hashcls = Hash_SHA512 + else: + raise ValueError("Bad PAChecksum2 checksum !") + + if hashcls().digest(text) != self.checksum.val: + raise ValueError("Bad PAChecksum2 checksum !") + + def make(self, text, h="sha256"): + """ + Make a checksum. + + :param text: the bytes to make a checksum of + """ + # Only some OIDs are supported. Dumb but readable code. + if h == "sha1": + hashcls = Hash_SHA + self.algorithmIdentifier.algorithm = ASN1_OID("1.3.14.3.2.26") + elif h == "sha256": + hashcls = Hash_SHA256 + self.algorithmIdentifier.algorithm = ASN1_OID("2.16.840.1.101.3.4.2.1") + elif h == "sha384": + hashcls = Hash_SHA384 + self.algorithmIdentifier.algorithm = ASN1_OID("2.16.840.1.101.3.4.2.2") + elif h == "sha512": + hashcls = Hash_SHA512 + self.algorithmIdentifier.algorithm = ASN1_OID("2.16.840.1.101.3.4.2.3") + else: + raise ValueError("Bad PAChecksum2 checksum !") + + self.checksum = ASN1_STRING(hashcls().digest(text)) + # still RFC 4556 sect 3.2.1 -class PKAuthenticator(ASN1_Packet): +class KRB_PKAuthenticator(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( Microseconds("cusec", 0, explicit_tag=0xA0), @@ -1292,14 +1377,34 @@ class PKAuthenticator(ASN1_Packet): ), # RFC8070 extension ASN1F_optional( - ASN1F_STRING("freshnessToken", "", explicit_tag=0xA4), + ASN1F_STRING("freshnessToken", None, explicit_tag=0xA4), ), # [MS-PKCA] sect 2.2.3 ASN1F_optional( - ASN1F_PACKET("paChecksum2", None, PAChecksum2, explicit_tag=0xA5), + ASN1F_PACKET("paChecksum2", PAChecksum2(), PAChecksum2, explicit_tag=0xA5), ), ) + def make_checksum(self, text, h="sha256"): + """ + Populate paChecksum and paChecksum2 + """ + # paChecksum (always sha-1) + self.paChecksum = ASN1_STRING(Hash_SHA().digest(text)) + + # paChecksum2 + self.paChecksum2 = PAChecksum2() + self.paChecksum2.make(text, h=h) + + def verify_checksum(self, text): + """ + Verify paChecksum and paChecksum2 + """ + if self.paChecksum.val != Hash_SHA().digest(text): + raise ValueError("Bad paChecksum checksum !") + + self.paChecksum2.verify(text) + # RFC8636 sect 6 @@ -1314,13 +1419,13 @@ class KDFAlgorithmId(ASN1_Packet): # still RFC 4556 sect 3.2.1 -class AuthPack(ASN1_Packet): +class KRB_AuthPack(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_PACKET( "pkAuthenticator", - PKAuthenticator(), - PKAuthenticator, + KRB_PKAuthenticator(), + KRB_PKAuthenticator, explicit_tag=0xA0, ), ASN1F_optional( @@ -1334,13 +1439,13 @@ class AuthPack(ASN1_Packet): ASN1F_optional( ASN1F_SEQUENCE_OF( "supportedCMSTypes", - [], + None, X509_AlgorithmIdentifier, explicit_tag=0xA2, ), ), ASN1F_optional( - ASN1F_STRING("clientDCNonce", None, explicit_tag=0xA3), + ASN1F_STRING("clientDHNonce", None, explicit_tag=0xA3), ), # RFC8636 extension ASN1F_optional( @@ -1349,7 +1454,7 @@ class AuthPack(ASN1_Packet): ) -_CMS_ENCAPSULATED["1.3.6.1.5.2.3.1"] = AuthPack +_CMS_ENCAPSULATED["1.3.6.1.5.2.3.1"] = KRB_AuthPack # sect 3.2.3 @@ -1709,6 +1814,12 @@ class KRB_AS_REP(ASN1_Packet): implicit_tag=ASN1_Class_KRB.AS_REP, ) + def getUPN(self): + return "%s@%s" % ( + self.cname.toString(), + self.crealm.val.decode(), + ) + class KRB_TGS_REP(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER @@ -2013,15 +2124,17 @@ def m2i(self, pkt, s): # 25: KDC_ERR_PREAUTH_REQUIRED # 36: KRB_AP_ERR_BADMATCH return MethodData(val[0].val, _underlayer=pkt), val[1] - elif pkt.errorCode.val in [6, 7, 12, 13, 18, 29, 41, 60]: + elif pkt.errorCode.val in [6, 7, 12, 13, 18, 29, 32, 41, 60, 62]: # 6: KDC_ERR_C_PRINCIPAL_UNKNOWN # 7: KDC_ERR_S_PRINCIPAL_UNKNOWN # 12: KDC_ERR_POLICY # 13: KDC_ERR_BADOPTION # 18: KDC_ERR_CLIENT_REVOKED # 29: KDC_ERR_SVC_UNAVAILABLE + # 32: KRB_AP_ERR_TKT_EXPIRED # 41: KRB_AP_ERR_MODIFIED # 60: KRB_ERR_GENERIC + # 62: KERB_ERR_TYPE_EXTENDED try: return KERB_ERROR_DATA(val[0].val, _underlayer=pkt), val[1] except BER_Decoding_Error: @@ -2112,9 +2225,10 @@ class KRB_ERROR(ASN1_Packet): 52: "KRB_ERR_RESPONSE_TOO_BIG", 60: "KRB_ERR_GENERIC", 61: "KRB_ERR_FIELD_TOOLONG", - 62: "KDC_ERROR_CLIENT_NOT_TRUSTED", - 63: "KDC_ERROR_KDC_NOT_TRUSTED", - 64: "KDC_ERROR_INVALID_SIG", + # RFC4556 + 62: "KDC_ERR_CLIENT_NOT_TRUSTED", + 63: "KDC_ERR_KDC_NOT_TRUSTED", + 64: "KDC_ERR_INVALID_SIG", 65: "KDC_ERR_KEY_TOO_WEAK", 66: "KDC_ERR_CERTIFICATE_MISMATCH", 67: "KRB_AP_ERR_NO_TGT", @@ -2127,6 +2241,11 @@ class KRB_ERROR(ASN1_Packet): 74: "KDC_ERR_REVOCATION_STATUS_UNAVAILABLE", 75: "KDC_ERR_CLIENT_NAME_MISMATCH", 76: "KDC_ERR_KDC_NAME_MISMATCH", + 77: "KDC_ERR_INCONSISTENT_KEY_PURPOSE", + 78: "KDC_ERR_DIGEST_IN_CERT_NOT_ACCEPTED", + 79: "KDC_ERR_PA_CHECKSUM_MUST_BE_INCLUDED", + 80: "KDC_ERR_DIGEST_IN_SIGNED_DATA_NOT_ACCEPTED", + 81: "KDC_ERR_PUBLIC_KEY_ENCRYPTION_NOT_SUPPORTED", # draft-ietf-kitten-iakerb 85: "KRB_AP_ERR_IAKERB_KDC_NOT_FOUND", 86: "KRB_AP_ERR_IAKERB_KDC_NO_RESPONSE", @@ -2313,11 +2432,11 @@ class KRB_AuthenticatorChecksum(Packet): }, ), ConditionalField( - LEShortField("DlgOpt", 0), + LEShortField("DlgOpt", 1), lambda pkt: pkt.Flags.GSS_C_DELEG_FLAG, ), ConditionalField( - FieldLenField("Dlgth", None, length_of="Deleg"), + FieldLenField("Dlgth", None, length_of="Deleg", fmt="I", Context.SendSeqNum) tok = KRB_InnerToken( @@ -4683,13 +5008,6 @@ def MakeToSign(Confounder, DecText): msgs[0].data = Data return msgs elif Context.KrbSessionKey.etype in [23, 24]: # RC4 - from scapy.libs.rfc3961 import ( - Cipher, - Hmac_MD5, - _rfc1964pad, - decrepit_algorithms, - ) - # Drop wrapping tok = signature.innerToken @@ -4747,7 +5065,7 @@ def MakeToSign(Confounder, DecText): def GSS_Init_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, target_name: Optional[str] = None, req_flags: Optional[GSS_C_FLAGS] = None, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, @@ -4756,8 +5074,6 @@ def GSS_Init_sec_context( # New context Context = self.CONTEXT(IsAcceptor=False, req_flags=req_flags) - from scapy.libs.rfc3961 import Key - if Context.state == self.STATE.INIT and self.U2U: # U2U - Get TGT Context.state = self.STATE.CLI_SENT_TGTREQ @@ -4776,59 +5092,84 @@ def GSS_Init_sec_context( if Context.state in [self.STATE.INIT, self.STATE.CLI_SENT_TGTREQ]: if not self.UPN: raise ValueError("Missing UPN attribute") + # Do we have a ST? if self.ST is None: # Client sends an AP-req if not self.SPN and not target_name: raise ValueError("Missing SPN/target_name attribute") additional_tickets = [] + if self.U2U: try: # GSSAPI / Kerberos - tgt_rep = token.root.innerToken.root + tgt_rep = input_token.root.innerToken.root except AttributeError: try: # Kerberos - tgt_rep = token.innerToken.root + tgt_rep = input_token.innerToken.root except AttributeError: return Context, None, GSS_S_DEFECTIVE_TOKEN if not isinstance(tgt_rep, KRB_TGT_REP): tgt_rep.show() - raise ValueError("KerberosSSP: Unexpected token !") + raise ValueError("KerberosSSP: Unexpected input_token !") additional_tickets = [tgt_rep.ticket] - if self.TGT is not None: - if not self.KEY: - raise ValueError("Cannot use TGT without the KEY") - # Use TGT - res = krb_tgs_req( - upn=self.UPN, - spn=self.SPN or target_name, - ip=self.DC_IP, - sessionkey=self.KEY, - ticket=self.TGT, - additional_tickets=additional_tickets, - u2u=self.U2U, - debug=self.debug, - ) - else: - # Ask for TGT then ST - res = krb_as_and_tgs( + + if self.TGT is None: + # Get TGT. We were passed a kerberos key + res = krb_as_req( upn=self.UPN, - spn=self.SPN or target_name, ip=self.DC_IP, key=self.KEY, password=self.PASSWORD, - additional_tickets=additional_tickets, - u2u=self.U2U, debug=self.debug, + verbose=bool(self.debug), ) + if res is None: + # Failed to retrieve the ticket + return Context, None, GSS_S_FAILURE + + # Update UPN (could have been canonicalized) + self.UPN = res.upn + + # Store TGT, + self.TGT = res.asrep.ticket + self.TGTSessionKey = res.sessionkey + else: + # We have a TGT and were passed its key + self.TGTSessionKey = self.KEY + + # Get ST + if not self.TGTSessionKey: + raise ValueError("Cannot use TGT without the KEY") + + res = krb_tgs_req( + upn=self.UPN, + spn=self.SPN or target_name, + ip=self.DC_IP, + sessionkey=self.TGTSessionKey, + ticket=self.TGT, + additional_tickets=additional_tickets, + u2u=self.U2U, + debug=self.debug, + verbose=bool(self.debug), + ) if not res: # Failed to retrieve the ticket return Context, None, GSS_S_FAILURE - self.ST, self.KEY = res.tgsrep.ticket, res.sessionkey + + # Store the service ticket and associated key + self.ST, Context.STSessionKey = res.tgsrep.ticket, res.sessionkey elif not self.KEY: raise ValueError("Must provide KEY with ST") - Context.STSessionKey = self.KEY + else: + # We were passed a ST and its key + Context.STSessionKey = self.KEY + + if Context.flags & GSS_C_FLAGS.GSS_C_DELEG_FLAG: + raise ValueError( + "Cannot use GSS_C_DELEG_FLAG when passed a service ticket !" + ) # Save ServerHostname if len(self.ST.sname.nameString) == 2: @@ -4860,25 +5201,47 @@ def GSS_Init_sec_context( # Get the realm of the client _, crealm = _parse_upn(self.UPN) + # Build the RFC4121 authenticator checksum + authenticator_checksum = KRB_AuthenticatorChecksum( + # RFC 4121 sect 4.1.1.2 + # "The Bnd field contains the MD5 hash of channel bindings" + Bnd=( + chan_bindings.digestMD5() + if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS + else (b"\x00" * 16) + ), + Flags=int(Context.flags), + ) + + if Context.flags & GSS_C_FLAGS.GSS_C_DELEG_FLAG: + # Delegate TGT + raise NotImplementedError("GSS_C_DELEG_FLAG is not implemented !") + # authenticator_checksum.Deleg = KRB_CRED( + # tickets=[self.TGT], + # encPart=EncryptedData() + # ) + # authenticator_checksum.encPart.encrypt( + # Context.STSessionKey, + # EncKrbCredPart( + # ticketInfo=KrbCredInfo( + # key=EncryptionKey.fromKey(self.TGTSessionKey), + # prealm=ASN1_GENERAL_STRING(crealm), + # pname=PrincipalName.fromUPN(self.UPN), + # # TODO: rework API to pass starttime... here. + # sreralm=self.TGT.realm, + # sname=self.TGT.sname, + # ) + # ) + # ) + # Build and encrypt the full KRB_Authenticator ap_req.authenticator.encrypt( Context.STSessionKey, KRB_Authenticator( crealm=crealm, cname=PrincipalName.fromUPN(self.UPN), - # RFC 4121 checksum cksum=Checksum( - cksumtype="KRB-AUTHENTICATOR", - checksum=KRB_AuthenticatorChecksum( - # RFC 4121 sect 4.1.1.2 - # "The Bnd field contains the MD5 hash of channel bindings" - Bnd=( - chan_bindings.digestMD5() - if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS - else (b"\x00" * 16) - ), - Flags=int(Context.flags), - ), + cksumtype="KRB-AUTHENTICATOR", checksum=authenticator_checksum ), ctime=ASN1_GENERALIZED_TIME(now_time), cusec=ASN1_INTEGER(0), @@ -4895,7 +5258,9 @@ def GSS_Init_sec_context( adData=KERB_AD_RESTRICTION_ENTRY( restriction=LSAP_TOKEN_INFO_INTEGRITY( MachineID=bytes(RandBin(32)), - PermanentMachineID=bytes(RandBin(32)), # noqa: E501 + PermanentMachineID=bytes( + RandBin(32) + ), ) ), ), @@ -4941,30 +5306,32 @@ def GSS_Init_sec_context( ) elif Context.state == self.STATE.CLI_SENT_APREQ: - if isinstance(token, KRB_AP_REP): + if isinstance(input_token, KRB_AP_REP): # Raw AP_REP was passed - ap_rep = token + ap_rep = input_token else: try: # GSSAPI / Kerberos - ap_rep = token.root.innerToken.root + ap_rep = input_token.root.innerToken.root except AttributeError: try: # Kerberos - ap_rep = token.innerToken.root + ap_rep = input_token.innerToken.root except AttributeError: try: # Raw kerberos DCE-STYLE - ap_rep = token.root + ap_rep = input_token.root except AttributeError: return Context, None, GSS_S_DEFECTIVE_TOKEN if not isinstance(ap_rep, KRB_AP_REP): return Context, None, GSS_S_DEFECTIVE_TOKEN + # Retrieve SessionKey repPart = ap_rep.encPart.decrypt(Context.STSessionKey) if repPart.subkey is not None: Context.SessionKey = repPart.subkey.keyvalue.val Context.KrbSessionKey = repPart.subkey.toKey() + # OK ! Context.state = self.STATE.CLI_RCVD_APREP if Context.flags & GSS_C_FLAGS.GSS_C_DCE_STYLE: @@ -4996,7 +5363,7 @@ def GSS_Init_sec_context( def GSS_Accept_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): @@ -5004,7 +5371,6 @@ def GSS_Accept_sec_context( # New context Context = self.CONTEXT(IsAcceptor=True, req_flags=req_flags) - from scapy.libs.rfc3961 import Key import scapy.layers.msrpce.mspac # noqa: F401 if Context.state == self.STATE.INIT: @@ -5023,21 +5389,21 @@ def GSS_Accept_sec_context( self.TGT, self.KEY = res.asrep.ticket, res.sessionkey # Server receives AP-req, sends AP-rep - if isinstance(token, KRB_AP_REQ): + if isinstance(input_token, KRB_AP_REQ): # Raw AP_REQ was passed - ap_req = token + ap_req = input_token else: try: # GSSAPI/Kerberos - ap_req = token.root.innerToken.root + ap_req = input_token.root.innerToken.root except AttributeError: try: # Kerberos - ap_req = token.innerToken.root + ap_req = input_token.innerToken.root except AttributeError: try: # Raw kerberos - ap_req = token.root + ap_req = input_token.root except AttributeError: return Context, None, GSS_S_DEFECTIVE_TOKEN @@ -5121,7 +5487,7 @@ def GSS_Accept_sec_context( ), ) ) - return Context, err, GSS_S_DEFECTIVE_TOKEN + return Context, err, GSS_S_DEFECTIVE_CREDENTIAL # Store information about the user in the Context if tkt.authorizationData and tkt.authorizationData.seq: @@ -5194,20 +5560,20 @@ def GSS_Accept_sec_context( # [MS-KILE] sect 3.4.5.1 # The server MUST receive the additional AP exchange reply message and # verify that the message is constructed correctly. - if not token: + if not input_token: return Context, None, GSS_S_DEFECTIVE_TOKEN # Server receives AP-req, sends AP-rep - if isinstance(token, KRB_AP_REP): + if isinstance(input_token, KRB_AP_REP): # Raw AP_REP was passed - ap_rep = token + ap_rep = input_token else: try: # GSSAPI/Kerberos - ap_rep = token.root.innerToken.root + ap_rep = input_token.root.innerToken.root except AttributeError: try: # Raw Kerberos - ap_rep = token.root + ap_rep = input_token.root except AttributeError: return Context, None, GSS_S_DEFECTIVE_TOKEN # Decrypt the AP-REP @@ -5223,7 +5589,7 @@ def GSS_Accept_sec_context( def GSS_Passive( self, Context: CONTEXT, - token=None, + input_token=None, req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, ): if Context is None: @@ -5236,25 +5602,31 @@ def GSS_Passive( and req_flags & GSS_C_FLAGS.GSS_C_DCE_STYLE ): Context, _, status = self.GSS_Accept_sec_context( - Context, token, req_flags=req_flags + Context, + input_token=input_token, + req_flags=req_flags, ) if status in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: Context.state = self.STATE.CLI_SENT_APREQ else: Context.state = self.STATE.FAILED - return Context, status elif Context.state == self.STATE.CLI_SENT_APREQ: Context, _, status = self.GSS_Init_sec_context( - Context, token, req_flags=req_flags + Context, + input_token=input_token, + req_flags=req_flags, ) if status == GSS_S_COMPLETE: + if req_flags & GSS_C_FLAGS.GSS_C_DCE_STYLE: + status = GSS_S_CONTINUE_NEEDED Context.state = self.STATE.SRV_SENT_APREP else: Context.state == self.STATE.FAILED - return Context, status + else: + # Unknown state. Don't crash though. + status = GSS_S_FAILURE - # Unknown state. Don't crash though. - return Context, GSS_S_FAILURE + return Context, status def GSS_Passive_set_Direction(self, Context: CONTEXT, IsAcceptor=False): if Context.IsAcceptor is not IsAcceptor: @@ -5287,6 +5659,3 @@ def MaximumSignatureLength(self, Context: CONTEXT): raise NotImplementedError else: return 28 - - def canMechListMIC(self, Context: CONTEXT): - return bool(Context.KrbSessionKey) diff --git a/scapy/layers/ldap.py b/scapy/layers/ldap.py index c09e07e64c1..38651dacd45 100644 --- a/scapy/layers/ldap.py +++ b/scapy/layers/ldap.py @@ -2070,7 +2070,7 @@ def bind( # 3. Second exchange: Response self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, - GSSAPI_BLOB(val), + input_token=GSSAPI_BLOB(val), target_name="ldap/" + self.host, chan_bindings=self.chan_bindings, ) @@ -2126,7 +2126,7 @@ def bind( break self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, - GSSAPI_BLOB(val), + input_token=GSSAPI_BLOB(val), target_name="ldap/" + self.host, chan_bindings=self.chan_bindings, ) diff --git a/scapy/layers/msrpce/msnrpc.py b/scapy/layers/msrpce/msnrpc.py index fef1007e562..5933889c2d5 100644 --- a/scapy/layers/msrpce/msnrpc.py +++ b/scapy/layers/msrpce/msnrpc.py @@ -22,6 +22,7 @@ NL_AUTH_MESSAGE, NL_AUTH_SIGNATURE, ) +from scapy.layers.kerberos import KerberosSSP, _parse_upn from scapy.layers.gssapi import ( GSS_C_FLAGS, GSS_C_NO_CHANNEL_BINDINGS, @@ -29,8 +30,9 @@ GSS_S_CONTINUE_NEEDED, GSS_S_FAILURE, GSS_S_FLAGS, + SSP, ) -from scapy.layers.ntlm import RC4, RC4K, RC4Init, SSP +from scapy.layers.ntlm import RC4, RC4K, RC4Init, MD4le from scapy.layers.msrpce.rpcclient import ( DCERPC_Client, @@ -40,6 +42,8 @@ from scapy.layers.msrpce.raw.ms_nrpc import ( NetrServerAuthenticate3_Request, NetrServerAuthenticate3_Response, + NetrServerAuthenticateKerberos_Request, + NetrServerAuthenticateKerberos_Response, NetrServerReqChallenge_Request, NetrServerReqChallenge_Response, NETLOGON_SECURE_CHANNEL_TYPE, @@ -52,8 +56,14 @@ from cryptography.hazmat.primitives import hashes, hmac from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from scapy.libs.rfc3961 import DES + + try: + # cryptography > 47.0 + from cryptography.hazmat.decrepit.ciphers.modes import CFB8 + except ImportError: + from cryptography.hazmat.primitives.ciphers.modes import CFB8 else: - hashes = hmac = Cipher = algorithms = modes = DES = None + hashes = hmac = Cipher = algorithms = modes = DES = CFB8 = None # Typing imports @@ -114,15 +124,17 @@ 0x00200000: "RODC-passthrough", # W: Supports Advanced Encryption Standard (AES) encryption and SHA2 hashing. 0x01000000: "AES", - # Supports Kerberos as the security support provider for secure channel setup. - 0x20000000: "Kerberos", + # Not used. MUST be ignored on receipt. + 0x20000000: "X", # Y: Supports Secure RPC. 0x40000000: "SecureRPC", - # Not used. MUST be ignored on receipt. - 0x80000000: "Z", + # Supports Kerberos as the security support provider for secure channel setup. + 0x80000000: "Kerberos", } _negotiateFlags = FlagsField("", 0, -32, _negotiateFlags).names +# -- CRYPTO + # [MS-NRPC] sect 3.1.4.3.1 @crypto_validator @@ -150,7 +162,7 @@ def ComputeSessionKeyStrongKey(HashNt, ClientChallenge, ServerChallenge): # [MS-NRPC] sect 3.1.4.4.1 @crypto_validator def ComputeNetlogonCredentialAES(Input, Sk): - cipher = Cipher(algorithms.AES(Sk), mode=modes.CFB8(b"\x00" * 16)) + cipher = Cipher(algorithms.AES(Sk), mode=CFB8(b"\x00" * 16)) encryptor = cipher.encryptor() return encryptor.update(Input) @@ -281,6 +293,9 @@ def __init__(self, SessionKey, computername, domainname, AES=True, **kwargs): self.domainname = domainname super(NetlogonSSP, self).__init__(**kwargs) + def GSS_Inquire_names_for_mech(self): + raise NotImplementedError("Netlogon cannot be used with SPNEGO !") + def _secure(self, Context, msgs, Seal): """ Internal function used by GSS_WrapEx and GSS_GetMICEx @@ -336,7 +351,7 @@ def _secure(self, Context, msgs, Seal): if Context.AES: IV = SequenceNumber * 2 encryptor = Cipher( - algorithms.AES(EncryptionKey), mode=modes.CFB8(IV) + algorithms.AES(EncryptionKey), mode=CFB8(IV) ).encryptor() # Confounder signature.Confounder = encryptor.update(Confounder) @@ -363,7 +378,7 @@ def _secure(self, Context, msgs, Seal): if Context.AES: EncryptionKey = self.SessionKey IV = signature.Checksum * 2 - cipher = Cipher(algorithms.AES(EncryptionKey), mode=modes.CFB8(IV)) + cipher = Cipher(algorithms.AES(EncryptionKey), mode=CFB8(IV)) encryptor = cipher.encryptor() signature.SequenceNumber = encryptor.update(SequenceNumber) else: @@ -395,7 +410,7 @@ def _unsecure(self, Context, msgs, signature, Seal): if Context.AES: EncryptionKey = self.SessionKey IV = signature.Checksum * 2 - cipher = Cipher(algorithms.AES(EncryptionKey), mode=modes.CFB8(IV)) + cipher = Cipher(algorithms.AES(EncryptionKey), mode=CFB8(IV)) decryptor = cipher.decryptor() SequenceNumber = decryptor.update(signature.SequenceNumber) else: @@ -426,7 +441,7 @@ def _unsecure(self, Context, msgs, signature, Seal): if Context.AES: IV = SequenceNumber * 2 decryptor = Cipher( - algorithms.AES(EncryptionKey), mode=modes.CFB8(IV) + algorithms.AES(EncryptionKey), mode=CFB8(IV) ).decryptor() # Confounder Confounder = decryptor.update(signature.Confounder) @@ -477,7 +492,7 @@ def GSS_VerifyMICEx(self, Context, msgs, signature): def GSS_Init_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, target_name: Optional[str] = None, req_flags: Optional[GSS_C_FLAGS] = None, chan_bindings: bytes = GSS_C_NO_CHANNEL_BINDINGS, @@ -503,7 +518,7 @@ def GSS_Init_sec_context( def GSS_Accept_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, chan_bindings: bytes = GSS_C_NO_CHANNEL_BINDINGS, ): @@ -569,8 +584,8 @@ class NetlogonClient(DCERPC_Client): >>> cli = NetlogonClient() >>> cli.connect_and_bind("192.168.0.100") >>> cli.establish_secure_channel( - ... domainname="DOMAIN", computername="WIN10", - ... HashNT=bytes.fromhex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + ... UPN="WIN10@DOMAIN", + ... HASHNT=bytes.fromhex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), ... ) """ @@ -583,26 +598,25 @@ def __init__( **kwargs, ): self.interface = find_dcerpc_interface("logon") - self.ndr64 = False # Netlogon doesn't work with NDR64 self.SessionKey = None self.ClientStoredCredential = None self.supportAES = supportAES super(NetlogonClient, self).__init__( DCERPC_Transport.NCACN_IP_TCP, auth_level=auth_level, - ndr64=self.ndr64, verb=verb, **kwargs, ) - def connect_and_bind(self, remoteIP): + def connect(self, host, **kwargs): """ This calls DCERPC_Client's connect_and_bind to bind the 'logon' interface. """ - super(NetlogonClient, self).connect_and_bind(remoteIP, self.interface) - - def alter_context(self): - return super(NetlogonClient, self).alter_context(self.interface) + super(NetlogonClient, self).connect( + host=host, + interface=self.interface, + **kwargs, + ) def create_authenticator(self): """ @@ -653,53 +667,56 @@ def validate_authenticator(self, auth): def establish_secure_channel( self, - computername: str, - domainname: str, - HashNt: bytes, + UPN: str, + DC_FQDN: str, + HASHNT: Optional[bytes] = None, + PASSWORD: Optional[str] = None, + KEY=None, + ssp: Optional[KerberosSSP] = None, mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3, secureChannelType=NETLOGON_SECURE_CHANNEL_TYPE.WorkstationSecureChannel, ): """ Function to establish the Netlogon Secure Channel. - This uses NetrServerAuthenticate3 to negotiate the session key, then creates a - NetlogonSSP that uses that session key and alters the DCE/RPC session to use it. + This uses NetrServerAuthenticate3 or NetrServerAuthenticateKerberos to + negotiate the session key, then creates a NetlogonSSP that uses that session + key and alters the DCE/RPC session to use it. :param mode: one of NETLOGON_SECURE_CHANNEL_METHOD. This defines which method to use to establish the secure channel. - :param computername: the netbios computer account name that is used to establish - the secure channel. (e.g. WIN10) - :param domainname: the netbios domain name to connect to (e.g. DOMAIN) - :param HashNt: the HashNT of the computer account. + :param UPN: the UPN of the computer account name that is used to establish + the secure channel. (e.g. WIN10$@domain.local) + :param DC_FQDN: the FQDN name of the DC. + + The function then requires one of the following: + + :param HASHNT: the HashNT of the computer account (in Authenticate3 mode). + :param KEY: a Kerberos key to use (in Kerberos mode) + :param PASSWORD: the password of the computer account (any mode). + :param ssp: a KerberosSSP to use (in Kerberos mode) """ - # Flow documented in 3.1.4 Session-Key Negotiation - # and sect 3.4.5.2 for specific calls - clientChall = os.urandom(8) - - # Step 1: NetrServerReqChallenge - netr_server_req_chall_response = self.sr1_req( - NetrServerReqChallenge_Request( - PrimaryName=None, - ComputerName=computername, - ClientChallenge=PNETLOGON_CREDENTIAL( - data=clientChall, - ), - ndr64=self.ndr64, - ndrendian=self.ndrendian, - ) - ) - if ( - NetrServerReqChallenge_Response not in netr_server_req_chall_response - or netr_server_req_chall_response.status != 0 - ): - print( - conf.color_theme.fail( - "! %s" - % STATUS_ERREF.get(netr_server_req_chall_response.status, "Failure") + computername, domainname = _parse_upn(UPN) + # We need to normalize here, since the functions require both the accountname + # and the normal (no dollar) computer name. + if computername.endswith("$"): + computername = computername[:-1] + + if mode == NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3: + if ssp or KEY: + raise ValueError("Cannot use 'ssp' on 'KEY' in Authenticate3 mode !") + if not HASHNT: + if PASSWORD: + HASHNT = MD4le(PASSWORD) + else: + raise ValueError("Missing either 'PASSWORD' or 'HASHNT' !") + if "." in domainname: + raise ValueError( + "The UPN in Authenticate3 must have a NETBIOS domain name !" ) - ) - netr_server_req_chall_response.show() - raise ValueError + else: + if HASHNT: + raise ValueError("Cannot use 'HASHNT' in Kerberos mode !") # Calc NegotiateFlags NegotiateFlags = FlagValue( @@ -712,23 +729,61 @@ def establish_secure_channel( # We are either using NetrServerAuthenticate3 or NetrServerAuthenticateKerberos if mode == NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3: # We use the legacy NetrServerAuthenticate3 function (NetlogonSSP) - # Step 2: Build the session key + + # Make sure the interface is bound + if not self.bind_or_alter(self.interface): + raise ValueError("Bind failed !") + + # Flow documented in 3.1.4 Session-Key Negotiation + # and sect 3.4.5.2 for specific calls + clientChall = os.urandom(8) + + # Perform NetrServerReqChallenge request + netr_server_req_chall_response = self.sr1_req( + NetrServerReqChallenge_Request( + PrimaryName=None, + ComputerName=computername, + ClientChallenge=PNETLOGON_CREDENTIAL( + data=clientChall, + ), + ndr64=self.ndr64, + ndrendian=self.ndrendian, + ) + ) + if ( + NetrServerReqChallenge_Response not in netr_server_req_chall_response + or netr_server_req_chall_response.status != 0 + ): + print( + conf.color_theme.fail( + "! %s" + % STATUS_ERREF.get( + netr_server_req_chall_response.status, "Failure" + ) + ) + ) + netr_server_req_chall_response.show() + raise ValueError("NetrServerReqChallenge failed !") + + # Build the session key serverChall = netr_server_req_chall_response.ServerChallenge.data if self.supportAES: - SessionKey = ComputeSessionKeyAES(HashNt, clientChall, serverChall) + SessionKey = ComputeSessionKeyAES(HASHNT, clientChall, serverChall) self.ClientStoredCredential = ComputeNetlogonCredentialAES( clientChall, SessionKey ) else: SessionKey = ComputeSessionKeyStrongKey( - HashNt, clientChall, serverChall + HASHNT, clientChall, serverChall ) self.ClientStoredCredential = ComputeNetlogonCredentialDES( clientChall, SessionKey ) + + # Perform Authenticate3 request netr_server_auth3_response = self.sr1_req( NetrServerAuthenticate3_Request( - PrimaryName=None, + PrimaryName="\\\\" + DC_FQDN, AccountName=computername + "$", SecureChannelType=secureChannelType, ComputerName=computername, @@ -740,10 +795,7 @@ def establish_secure_channel( ndrendian=self.ndrendian, ) ) - if ( - NetrServerAuthenticate3_Response not in netr_server_auth3_response - or netr_server_auth3_response.status != 0 - ): + if netr_server_auth3_response.status != 0: # An error occurred. NegotiatedFlags = None if NetrServerAuthenticate3_Response in netr_server_auth3_response: @@ -758,20 +810,8 @@ def establish_secure_channel( % (NegotiatedFlags ^ NegotiateFlags) ) ) + raise ValueError("NetrServerAuthenticate3 failed !") - # Show the error - print( - conf.color_theme.fail( - "! %s" - % STATUS_ERREF.get(netr_server_auth3_response.status, "Failure") - ) - ) - - # If error is unknown, show the packet entirely - if netr_server_auth3_response.status not in STATUS_ERREF: - netr_server_auth3_response.show() - - raise ValueError # Check Server Credential if self.supportAES: if ( @@ -798,10 +838,48 @@ def establish_secure_channel( domainname=domainname, computername=computername, ) + + # Finally alter context (to use the SSP) + if not self.alter_context(self.interface): + raise ValueError("Bind failed !") + elif mode == NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticateKerberos: + # We use the brand new NetrServerAuthenticateKerberos function NegotiateFlags += "Kerberos" - # TODO - raise NotImplementedError - # Finally alter context (to use the SSP) - self.alter_context() + # Set KerberosSSP and alter context + if ssp: + self.ssp = self.sock.session.ssp = ssp + else: + self.ssp = self.sock.session.ssp = KerberosSSP( + UPN=UPN, + SPN="netlogon/" + DC_FQDN, + PASSWORD=PASSWORD, + KEY=KEY, + ) + if not self.bind_or_alter(self.interface): + raise ValueError("Bind failed !") + + # Send AuthenticateKerberos request + netr_server_authkerb_response = self.sr1_req( + NetrServerAuthenticateKerberos_Request( + PrimaryName="\\\\" + DC_FQDN, + AccountName=computername + "$", + AccountType=secureChannelType, + ComputerName=computername, + NegotiateFlags=int(NegotiateFlags), + ndr64=self.ndr64, + ndrendian=self.ndrendian, + ) + ) + if ( + NetrServerAuthenticateKerberos_Response + not in netr_server_authkerb_response + or netr_server_authkerb_response.status != 0 + ): + # An error occurred + netr_server_authkerb_response.show() + raise ValueError("NetrServerAuthenticateKerberos failed !") + + # The NRPC session key is in this case the kerberos one + self.SessionKey = self.sspcontext.SessionKey diff --git a/scapy/layers/msrpce/rpcclient.py b/scapy/layers/msrpce/rpcclient.py index a25f6587126..e3a1d46a435 100644 --- a/scapy/layers/msrpce/rpcclient.py +++ b/scapy/layers/msrpce/rpcclient.py @@ -41,6 +41,7 @@ find_dcerpc_interface, NDRContextHandle, NDRPointer, + RPC_C_IMP_LEVEL, ) from scapy.layers.gssapi import ( SSP, @@ -80,6 +81,7 @@ class DCERPC_Client(object): :param ndrendian: the endianness to use (default little) :param verb: enable verbose logging (default True) :param auth_level: the DCE_C_AUTHN_LEVEL to use + :param impersonation_type: the RPC_C_IMP_LEVEL to use """ def __init__( @@ -89,7 +91,7 @@ def __init__( ndrendian: str = "little", verb: bool = True, auth_level: Optional[DCE_C_AUTHN_LEVEL] = None, - auth_context_id: int = 0, + impersonation_type: RPC_C_IMP_LEVEL = RPC_C_IMP_LEVEL.DEFAULT, **kwargs, ): self.sock = None @@ -100,7 +102,8 @@ def __init__( # Counters self.call_id = 0 - self.all_cont_id = 0 # number of contexts sent + self.next_cont_id = 0 # next available context id + self.next_auth_contex_id = 0 # next available auth context id # Session parameters if ndr64 is None: @@ -118,8 +121,12 @@ def __init__( self.auth_level = DCE_C_AUTHN_LEVEL.CONNECT else: self.auth_level = DCE_C_AUTHN_LEVEL.NONE - self.auth_context_id = auth_context_id + if impersonation_type == RPC_C_IMP_LEVEL.DEFAULT: + # Same default as windows + impersonation_type = RPC_C_IMP_LEVEL.IDENTIFY + self.impersonation_type = impersonation_type self._first_time_on_interface = True + self.contexts = {} self.dcesockargs = kwargs self.dcesockargs["transport"] = self.transport @@ -135,7 +142,6 @@ def from_smblink(cls, smbcli, smb_kwargs={}, **kwargs): DceRpc5, ssp=client.ssp, auth_level=client.auth_level, - auth_context_id=client.auth_context_id, **client.dcesockargs, ) return client @@ -144,23 +150,71 @@ def from_smblink(cls, smbcli, smb_kwargs={}, **kwargs): def session(self) -> DceRpcSession: return self.sock.session - def connect(self, host, port=None, timeout=5, smb_kwargs={}): + def connect( + self, + host, + endpoint: Union[int, str] = None, + port: Optional[int] = None, + interface=None, + timeout=5, + smb_kwargs={}, + ): """ Initiate a connection. :param host: the host to connect to - :param port: (optional) the port to connect to + :param endpoint: (optional) the port/smb pipe to connect to + :param interface: (optional) if endpoint isn't provided, uses the endpoint + mapper to find the appropriate endpoint for that interface. :param timeout: (optional) the connection timeout (default 5) + :param port: (optional) the port to connect to. (useful for SMB) """ + if endpoint is None and interface is not None: + # Figure out the endpoint using the endpoint mapper + + if self.transport == DCERPC_Transport.NCACN_IP_TCP and port is None: + # IP/TCP + # ask the endpoint mapper (port 135) for the IP:PORT + endpoints = get_endpoint( + host, + interface, + ndrendian=self.ndrendian, + verb=self.verb, + ) + if endpoints: + _, endpoint = endpoints[0] + else: + raise ValueError( + "Could not find an available endpoint for that interface !" + ) + elif self.transport == DCERPC_Transport.NCACN_NP: + # SMB + # ask the endpoint mapper (over SMB) for the namedpipe + endpoints = get_endpoint( + host, + interface, + transport=self.transport, + ndrendian=self.ndrendian, + verb=self.verb, + smb_kwargs=smb_kwargs, + ) + if endpoints: + endpoint = endpoints[0].lstrip("\\pipe\\") + else: + return + + # Assign the default port if no port is provided if port is None: if self.transport == DCERPC_Transport.NCACN_IP_TCP: # IP/TCP - port = 135 + port = endpoint or 135 elif self.transport == DCERPC_Transport.NCACN_NP: # SMB port = 445 else: raise ValueError( "Can't guess the port for transport: %s" % self.transport ) + + # Start socket and connect self.host = host self.port = port sock = socket.socket() @@ -177,7 +231,12 @@ def connect(self, host, port=None, timeout=5, smb_kwargs={}): "\u2514 Connected from %s" % repr(sock.getsockname()) ) ) + if self.transport == DCERPC_Transport.NCACN_NP: # SMB + # If the endpoint is provided, connect to it. + if endpoint is not None: + self.open_smbpipe(endpoint) + # We pack the socket into a SMB_RPC_SOCKET sock = self.smbrpcsock = SMB_RPC_SOCKET.from_tcpsock( sock, ssp=self.ssp, **smb_kwargs @@ -189,7 +248,6 @@ def connect(self, host, port=None, timeout=5, smb_kwargs={}): DceRpc5, ssp=self.ssp, auth_level=self.auth_level, - auth_context_id=self.auth_context_id, **self.dcesockargs, ) @@ -347,10 +405,15 @@ def _get_bind_context(self, interface): """ Internal: get the bind DCE/RPC context. """ + if interface in self.contexts: + # We have already found acceptable contexts for this interface, + # reuse that. + return self.contexts[interface] + # NDR 2.0 contexts = [ DceRpc5Context( - cont_id=self.all_cont_id, + cont_id=self.next_cont_id, abstract_syntax=DceRpc5AbstractSyntax( if_uuid=interface.uuid, if_version=interface.if_version, @@ -364,13 +427,13 @@ def _get_bind_context(self, interface): ], ), ] - self.all_cont_id += 1 + self.next_cont_id += 1 # NDR64 if self.ndr64: contexts.append( DceRpc5Context( - cont_id=self.all_cont_id, + cont_id=self.next_cont_id, abstract_syntax=DceRpc5AbstractSyntax( if_uuid=interface.uuid, if_version=interface.if_version, @@ -384,12 +447,12 @@ def _get_bind_context(self, interface): ], ) ) - self.all_cont_id += 1 + self.next_cont_id += 1 # BindTimeFeatureNegotiationBitmask contexts.append( DceRpc5Context( - cont_id=self.all_cont_id, + cont_id=self.next_cont_id, abstract_syntax=DceRpc5AbstractSyntax( if_uuid=interface.uuid, if_version=interface.if_version, @@ -402,11 +465,28 @@ def _get_bind_context(self, interface): ], ) ) - self.all_cont_id += 1 + self.next_cont_id += 1 + + # Store contexts for this interface + self.contexts[interface] = contexts return contexts - def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls): + def _check_bind_context(self, interface, contexts) -> bool: + """ + Internal: check the answer DCE/RPC bind context, and update them. + """ + for i, ctx in enumerate(contexts): + if ctx.result == 0: + # Context was accepted. Remove all others from cache + self.contexts[interface] = [self.contexts[interface][i]] + return True + + return False + + def _bind( + self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls + ) -> bool: """ Internal: used to send a bind/alter request """ @@ -418,6 +498,7 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls + (" (with %s)" % self.ssp.__class__.__name__ if self.ssp else "") ) ) + # Do we need an authenticated bind if not self.ssp or ( self.sspcontext is not None @@ -452,13 +533,25 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls if self.auth_level >= DCE_C_AUTHN_LEVEL.PKT_PRIVACY else 0 ) + | ( + GSS_C_FLAGS.GSS_C_IDENTIFY_FLAG + if self.impersonation_type <= RPC_C_IMP_LEVEL.IDENTIFY + else 0 + ) + | ( + GSS_C_FLAGS.GSS_C_DELEG_FLAG + if self.impersonation_type == RPC_C_IMP_LEVEL.DELEGATE + else 0 + ) ), target_name="host/" + self.host, ) + if status not in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: # Authentication failed. self.sspcontext.clifailure() return False + resp = self.sr1( reqcls(context_elem=self._get_bind_context(interface)), auth_verifier=( @@ -467,7 +560,7 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls else CommonAuthVerifier( auth_type=self.ssp.auth_type, auth_level=self.auth_level, - auth_context_id=self.auth_context_id, + auth_context_id=self.session.auth_context_id, auth_value=token, ) ), @@ -481,16 +574,21 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls ) ), ) - if respcls not in resp: + + # Check that the answer looks valid and contexts were accepted + if respcls not in resp or not self._check_bind_context( + interface, resp.results + ): token = None status = GSS_S_FAILURE else: # Call the underlying SSP self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, - token=resp.auth_verifier.auth_value, + input_token=resp.auth_verifier.auth_value, target_name="host/" + self.host, ) + if status in [GSS_S_CONTINUE_NEEDED, GSS_S_COMPLETE]: # Authentication should continue, in two ways: # - through DceRpc5Auth3 (e.g. NTLM) @@ -503,7 +601,7 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls auth_verifier=CommonAuthVerifier( auth_type=self.ssp.auth_type, auth_level=self.auth_level, - auth_context_id=self.auth_context_id, + auth_context_id=self.session.auth_context_id, auth_value=token, ), ) @@ -518,7 +616,7 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls auth_verifier=CommonAuthVerifier( auth_type=self.ssp.auth_type, auth_level=self.auth_level, - auth_context_id=self.auth_context_id, + auth_context_id=self.session.auth_context_id, auth_value=token, ), ) @@ -530,22 +628,22 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls break self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( self.sspcontext, - token=resp.auth_verifier.auth_value, + input_token=resp.auth_verifier.auth_value, target_name="host/" + self.host, ) else: log_runtime.error("GSS_Init_sec_context failed with %s !" % status) + # Check context acceptance if ( status == GSS_S_COMPLETE and respcls in resp - and any(x.result == 0 for x in resp.results[: int(self.ndr64) + 1]) + and self._check_bind_context(interface, resp.results) ): self.call_id = 0 # reset call id port = resp.sec_addr.port_spec.decode() ndr = self.session.ndr64 and "NDR64" or "NDR32" self.ndr64 = self.session.ndr64 - self.cont_id = int(self.session.ndr64) # ctx 0 for NDR32, 1 for NDR64 if self.verb: print( conf.color_theme.success( @@ -592,7 +690,7 @@ def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls resp.show() return False - def bind(self, interface: Union[DceRpcInterface, ComInterface]): + def bind(self, interface: Union[DceRpcInterface, ComInterface]) -> bool: """ Bind the client to an interface @@ -600,7 +698,7 @@ def bind(self, interface: Union[DceRpcInterface, ComInterface]): """ return self._bind(interface, DceRpc5Bind, DceRpc5BindAck) - def alter_context(self, interface: Union[DceRpcInterface, ComInterface]): + def alter_context(self, interface: Union[DceRpcInterface, ComInterface]) -> bool: """ Alter context: post-bind context negotiation @@ -608,7 +706,7 @@ def alter_context(self, interface: Union[DceRpcInterface, ComInterface]): """ return self._bind(interface, DceRpc5AlterContext, DceRpc5AlterContextResp) - def bind_or_alter(self, interface: Union[DceRpcInterface, ComInterface]): + def bind_or_alter(self, interface: Union[DceRpcInterface, ComInterface]) -> bool: """ Bind the client to an interface or alter the context if already bound @@ -616,10 +714,11 @@ def bind_or_alter(self, interface: Union[DceRpcInterface, ComInterface]): """ if not self.session.rpc_bind_interface: # No interface is bound - self.bind(interface) + return self.bind(interface) elif self.session.rpc_bind_interface != interface: # An interface is already bound - self.alter_context(interface) + return self.alter_context(interface) + return True def open_smbpipe(self, name: str): """ @@ -640,7 +739,7 @@ def close_smbpipe(self): def connect_and_bind( self, - ip: str, + host: str, interface: DceRpcInterface, port: Optional[int] = None, timeout: int = 5, @@ -650,45 +749,20 @@ def connect_and_bind( Asks the Endpoint Mapper what address to use to connect to the interface, then uses connect() followed by a bind() - :param ip: the ip to connect to + :param host: the host to connect to :param interface: the DceRpcInterface object :param port: (optional, NCACN_NP only) the port to connect to :param timeout: (optional) the connection timeout (default 5) """ - if self.transport == DCERPC_Transport.NCACN_IP_TCP: - # IP/TCP - # 1. ask the endpoint mapper (port 135) for the IP:PORT - endpoints = get_endpoint( - ip, - interface, - ndrendian=self.ndrendian, - verb=self.verb, - ) - if endpoints: - ip, port = endpoints[0] - else: - return - # 2. Connect to that IP:PORT - self.connect(ip, port=port, timeout=timeout) - elif self.transport == DCERPC_Transport.NCACN_NP: - # SMB - # 1. ask the endpoint mapper (over SMB) for the namedpipe - endpoints = get_endpoint( - ip, - interface, - transport=self.transport, - ndrendian=self.ndrendian, - verb=self.verb, - smb_kwargs=smb_kwargs, - ) - if endpoints: - pipename = endpoints[0].lstrip("\\pipe\\") - else: - return - # 2. connect to the SMB server - self.connect(ip, port=port, timeout=timeout, smb_kwargs=smb_kwargs) - # 3. open the new named pipe - self.open_smbpipe(pipename) + # Connect to the interface using the endpoint mapper + self.connect( + host=host, + interface=interface, + port=port, + timeout=timeout, + smb_kwargs=smb_kwargs, + ) + # Bind in RPC self.bind(interface) @@ -861,15 +935,24 @@ def get_endpoint( """ client = DCERPC_Client( transport, + # EPM only works with NDR32 ndr64=False, ndrendian=ndrendian, verb=verb, ssp=ssp, - ) # EPM only works with NDR32 - client.connect(ip, smb_kwargs=smb_kwargs) - if transport == DCERPC_Transport.NCACN_NP: # SMB - client.open_smbpipe("epmapper") + ) + + if transport == DCERPC_Transport.NCACN_IP_TCP: + endpoint = 135 + elif transport == DCERPC_Transport.NCACN_NP: + endpoint = "epmapper" + else: + raise ValueError("Unknown transport value !") + + client.connect(ip, endpoint=endpoint, smb_kwargs=smb_kwargs) + client.bind(find_dcerpc_interface("ept")) endpoints = client.epm_map(interface) + client.close() return endpoints diff --git a/scapy/layers/msrpce/rpcserver.py b/scapy/layers/msrpce/rpcserver.py index 5a8435cac17..767d8f09c0c 100644 --- a/scapy/layers/msrpce/rpcserver.py +++ b/scapy/layers/msrpce/rpcserver.py @@ -377,9 +377,8 @@ def recv(self, data): print( conf.color_theme.success( f">> {cls.__name__} {self.session.rpc_bind_interface.name}" - f" is on port '{port_spec.decode()}' using " + ( - "NDR64" if self.ndr64 else "NDR32" - ) + f" is on port '{port_spec.decode()}' using " + + ("NDR64" if self.ndr64 else "NDR32") ) ) elif DceRpc5Request in req: diff --git a/scapy/layers/ntlm.py b/scapy/layers/ntlm.py index 556faee0ea5..60258eb6be4 100644 --- a/scapy/layers/ntlm.py +++ b/scapy/layers/ntlm.py @@ -60,6 +60,8 @@ from scapy.sessions import StringBuffer from scapy.layers.gssapi import ( + _GSSAPI_OIDS, + _GSSAPI_SIGNATURE_OIDS, GSS_C_FLAGS, GSS_C_NO_CHANNEL_BINDINGS, GSS_S_BAD_BINDINGS, @@ -70,8 +72,6 @@ GSS_S_FLAGS, GssChannelBindings, SSP, - _GSSAPI_OIDS, - _GSSAPI_SIGNATURE_OIDS, ) # Typing imports @@ -94,6 +94,18 @@ ########## +# NTLM structures are all in all very complicated. Many fields don't have a fixed +# position, but are rather referred to with an offset (from the beginning of the +# structure) and a length. In addition to that, there are variants of the structure +# with missing fields when running old versions of Windows (sometimes also seen when +# talking to products that reimplement NTLM, most notably backup applications). + +# We add `_NTLMPayloadField` and `_NTLMPayloadPacket` to parse fields that use an +# offset, and `_NTLM_post_build` to be able to rebuild those offsets. +# In addition, the `NTLM_VARIANT*` allows to select what flavor of NTLM to use +# (NT, XP, or Recent). But in real world use only Recent should be used. + + class _NTLMPayloadField(_StrField[List[Tuple[str, Any]]]): """Special field used to dissect NTLM payloads. This isn't trivial because the offsets are variable.""" @@ -396,6 +408,41 @@ def _NTLM_post_build(self, p, pay_offset, fields, config=_NTLM_CONFIG): ############## +# -- Util: VARIANT class + + +class NTLM_VARIANT(IntEnum): + """ + The message variant to use for NTLM. + """ + + NT_OR_2000 = 0 + XP_OR_2003 = 1 + RECENT = 2 + + +class _NTLM_VARIANT_Packet(_NTLMPayloadPacket): + def __init__(self, *args, **kwargs): + self.VARIANT = kwargs.pop("VARIANT", NTLM_VARIANT.RECENT) + super(_NTLM_VARIANT_Packet, self).__init__(*args, **kwargs) + + def clone_with(self, *args, **kwargs): + pkt = super(_NTLM_VARIANT_Packet, self).clone_with(*args, **kwargs) + pkt.VARIANT = self.VARIANT + return pkt + + def copy(self): + pkt = super(_NTLM_VARIANT_Packet, self).copy() + pkt.VARIANT = self.VARIANT + + return pkt + + def show2(self, dump=False, indent=3, lvl="", label_lvl=""): + return self.__class__(bytes(self), VARIANT=self.VARIANT).show( + dump, indent, lvl, label_lvl + ) + + # Sect 2.2 @@ -406,13 +453,17 @@ class NTLM_Header(Packet): LEIntEnumField( "MessageType", 3, - {1: "NEGOTIATE_MESSAGE", 2: "CHALLENGE_MESSAGE", 3: "AUTHENTICATE_MESSAGE"}, + { + 1: "NEGOTIATE_MESSAGE", + 2: "CHALLENGE_MESSAGE", + 3: "AUTHENTICATE_MESSAGE", + }, ), ] @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): - if _pkt and len(_pkt) >= 10: + if cls is NTLM_Header and _pkt and len(_pkt) >= 10: MessageType = struct.unpack(" 32) and 40 or 32) + OFFSET = lambda pkt: ( + 32 + if ( + pkt.VARIANT == NTLM_VARIANT.NT_OR_2000 + or (pkt.DomainNameBufferOffset or 40) <= 32 + ) + else 40 + ) fields_desc = ( [ NTLM_Header, @@ -510,15 +569,18 @@ class NTLM_NEGOTIATE(_NTLMPayloadPacket): ConditionalField( # (not present on some old Windows versions. We use a heuristic) x, - lambda pkt: ( + lambda pkt: pkt.VARIANT >= NTLM_VARIANT.XP_OR_2003 + and ( ( - 40 - if pkt.DomainNameBufferOffset is None - else pkt.DomainNameBufferOffset or len(pkt.original or b"") + ( + 40 + if pkt.DomainNameBufferOffset is None + else pkt.DomainNameBufferOffset or len(pkt.original or b"") + ) + > 32 ) - > 32 - ) - or pkt.fields.get(x.name, b""), + or pkt.fields.get(x.name, b"") + ), ) for x in _NTLM_Version.fields_desc ] @@ -556,12 +618,42 @@ def post_build(self, pkt, pay): class Single_Host_Data(Packet): fields_desc = [ - LEIntField("Size", 48), + LEIntField("Size", None), LEIntField("Z4", 0), - XStrFixedLenField("CustomData", b"", length=8), + # "CustomData" guessed using LSAP_TOKEN_INFO_INTEGRITY. + FlagsField( + "Flags", + 0, + -32, + { + 0x00000001: "UAC-Restricted", + }, + ), + LEIntEnumField( + "TokenIL", + 0x00002000, + { + 0x00000000: "Untrusted", + 0x00001000: "Low", + 0x00002000: "Medium", + 0x00003000: "High", + 0x00004000: "System", + 0x00005000: "Protected process", + }, + ), XStrFixedLenField("MachineID", b"", length=32), + # KB 5068222 - still waiting for [MS-KILE] update (oct. 2025) + ConditionalField( + XStrFixedLenField("PermanentMachineID", None, length=32), + lambda pkt: pkt.Size is None or pkt.Size > 48, + ), ] + def post_build(self, pkt, pay): + if self.Size is None: + pkt = struct.pack(" 48) and 56 or 48) + OFFSET = lambda pkt: ( + 48 + if ( + pkt.VARIANT == NTLM_VARIANT.NT_OR_2000 + or (pkt.TargetInfoBufferOffset or 56) <= 48 + ) + else 56 + ) fields_desc = ( [ NTLM_Header, @@ -653,8 +753,11 @@ class NTLM_CHALLENGE(_NTLMPayloadPacket): ConditionalField( # (not present on some old Windows versions. We use a heuristic) x, - lambda pkt: ((pkt.TargetInfoBufferOffset or 56) > 40) - or pkt.fields.get(x.name, b""), + lambda pkt: pkt.VARIANT >= NTLM_VARIANT.XP_OR_2003 + and ( + ((pkt.TargetInfoBufferOffset or 56) > 40) + or pkt.fields.get(x.name, b"") + ), ) for x in _NTLM_Version.fields_desc ] @@ -770,14 +873,23 @@ def computeNTProofStr(self, ResponseKeyNT, ServerChallenge): return HMAC_MD5(ResponseKeyNT, ServerChallenge + temp) -class NTLM_AUTHENTICATE(_NTLMPayloadPacket): +class NTLM_AUTHENTICATE(_NTLM_VARIANT_Packet, NTLM_Header): name = "NTLM Authenticate" + __slots__ = ["VARIANT"] MessageType = 3 NTLM_VERSION = 1 OFFSET = lambda pkt: ( - ((pkt.DomainNameBufferOffset or 88) <= 64) - and 64 - or (((pkt.DomainNameBufferOffset or 88) > 72) and 88 or 72) + 64 + if ( + pkt.VARIANT == NTLM_VARIANT.NT_OR_2000 + or (pkt.DomainNameBufferOffset or 88) <= 64 + ) + else ( + 72 + if pkt.VARIANT == NTLM_VARIANT.XP_OR_2003 + or ((pkt.DomainNameBufferOffset or 88) <= 72) + else 88 + ) ) fields_desc = ( [ @@ -814,8 +926,11 @@ class NTLM_AUTHENTICATE(_NTLMPayloadPacket): ConditionalField( # (not present on some old Windows versions. We use a heuristic) x, - lambda pkt: ((pkt.DomainNameBufferOffset or 88) > 64) - or pkt.fields.get(x.name, b""), + lambda pkt: pkt.VARIANT >= NTLM_VARIANT.XP_OR_2003 + and ( + ((pkt.DomainNameBufferOffset or 88) > 64) + or pkt.fields.get(x.name, b"") + ), ) for x in _NTLM_Version.fields_desc ] @@ -824,8 +939,11 @@ class NTLM_AUTHENTICATE(_NTLMPayloadPacket): ConditionalField( # (not present on some old Windows versions. We use a heuristic) XStrFixedLenField("MIC", b"", length=16), - lambda pkt: ((pkt.DomainNameBufferOffset or 88) > 72) - or pkt.fields.get("MIC", b""), + lambda pkt: pkt.VARIANT >= NTLM_VARIANT.RECENT + and ( + ((pkt.DomainNameBufferOffset or 88) > 72) + or pkt.fields.get("MIC", b"") + ), ), # Payload _NTLMPayloadField( @@ -1190,7 +1308,6 @@ class NTLMSSP(SSP): authenticates inbound users. """ - oid = "1.3.6.1.4.1.311.2.2.10" auth_type = 0x0A class STATE(SSP.STATE): @@ -1215,6 +1332,7 @@ class CONTEXT(SSP.CONTEXT): "neg_tok", "chall_tok", "ServerHostname", + "ServerDomain", ] def __init__(self, IsAcceptor, req_flags=None): @@ -1232,6 +1350,7 @@ def __init__(self, IsAcceptor, req_flags=None): self.neg_tok = None self.chall_tok = None self.ServerHostname = None + self.ServerDomain = None self.IsAcceptor = IsAcceptor super(NTLMSSP.CONTEXT, self).__init__(req_flags=req_flags) @@ -1241,12 +1360,16 @@ def clifailure(self): def __repr__(self): return "NTLMSSP" + # [MS-NLMP] note <36>: "the maximum lifetime is 36 hours" (lol, Kerberos has 5min) + NTLM_MaxLifetime = 36 * 3600 + def __init__( self, UPN=None, HASHNT=None, PASSWORD=None, USE_MIC=True, + VARIANT: NTLM_VARIANT = NTLM_VARIANT.RECENT, NTLM_VALUES={}, DOMAIN_FQDN=None, DOMAIN_NB_NAME=None, @@ -1261,9 +1384,17 @@ def __init__( if HASHNT is None and PASSWORD is not None: HASHNT = MD4le(PASSWORD) self.HASHNT = HASHNT - self.USE_MIC = USE_MIC + self.VARIANT = VARIANT + if self.VARIANT != NTLM_VARIANT.RECENT: + log_runtime.warning( + "VARIANT != NTLM_VARIANT.RECENT. You shouldn't touch this !" + ) + self.USE_MIC = False + else: + self.USE_MIC = USE_MIC self.NTLM_VALUES = NTLM_VALUES if UPN is not None: + # Populate values used only in server mode. from scapy.layers.kerberos import _parse_upn try: @@ -1274,14 +1405,17 @@ def __init__( COMPUTER_NB_NAME = user except ValueError: pass + + # Compute various netbios/fqdn names self.DOMAIN_FQDN = DOMAIN_FQDN or "domain.local" self.DOMAIN_NB_NAME = ( DOMAIN_NB_NAME or self.DOMAIN_FQDN.split(".")[0].upper()[:15] ) - self.COMPUTER_NB_NAME = COMPUTER_NB_NAME or "SRV" + self.COMPUTER_NB_NAME = COMPUTER_NB_NAME or "WIN10" self.COMPUTER_FQDN = COMPUTER_FQDN or ( self.COMPUTER_NB_NAME.lower() + "." + self.DOMAIN_FQDN ) + self.IDENTITIES = IDENTITIES self.DO_NOT_CHECK_LOGIN = DO_NOT_CHECK_LOGIN self.SERVER_CHALLENGE = SERVER_CHALLENGE @@ -1290,6 +1424,9 @@ def __init__( def LegsAmount(self, Context: CONTEXT): return 3 + def GSS_Inquire_names_for_mech(self): + return ["1.3.6.1.4.1.311.2.2.10"] + def GSS_GetMICEx(self, Context, msgs, qop_req=0): """ [MS-NLMP] sect 3.4.8 @@ -1349,18 +1486,18 @@ def GSS_UnwrapEx(self, Context, msgs, signature): self.GSS_VerifyMICEx(Context, msgs, signature) return msgs - def canMechListMIC(self, Context): + def SupportsMechListMIC(self): if not self.USE_MIC: # RFC 4178 # "If the mechanism selected by the negotiation does not support integrity # protection, then no mechlistMIC token is used." return False - if not Context or not Context.SessionKey: - # Not available yet + if self.DO_NOT_CHECK_LOGIN: + # In this mode, we won't negotiate any credentials. return False return True - def getMechListMIC(self, Context, input): + def GetMechListMIC(self, Context, input): # [MS-SPNG] # "When NTLM is negotiated, the SPNG server MUST set OriginalHandle to # ServerHandle before generating the mechListMIC, then set ServerHandle to @@ -1368,11 +1505,11 @@ def getMechListMIC(self, Context, input): OriginalHandle = Context.SendSealHandle Context.SendSealHandle = RC4Init(Context.SendSealKey) try: - return super(NTLMSSP, self).getMechListMIC(Context, input) + return super(NTLMSSP, self).GetMechListMIC(Context, input) finally: Context.SendSealHandle = OriginalHandle - def verifyMechListMIC(self, Context, otherMIC, input): + def VerifyMechListMIC(self, Context, otherMIC, input): # [MS-SPNG] # "the SPNEGO Extension server MUST set OriginalHandle to ClientHandle before # validating the mechListMIC and then set ClientHandle to OriginalHandle after @@ -1380,14 +1517,14 @@ def verifyMechListMIC(self, Context, otherMIC, input): OriginalHandle = Context.RecvSealHandle Context.RecvSealHandle = RC4Init(Context.RecvSealKey) try: - return super(NTLMSSP, self).verifyMechListMIC(Context, otherMIC, input) + return super(NTLMSSP, self).VerifyMechListMIC(Context, otherMIC, input) finally: Context.RecvSealHandle = OriginalHandle def GSS_Init_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, target_name: Optional[str] = None, req_flags: Optional[GSS_C_FLAGS] = None, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, @@ -1399,6 +1536,7 @@ def GSS_Init_sec_context( # Client: negotiate # Create a default token tok = NTLM_NEGOTIATE( + VARIANT=self.VARIANT, NegotiateFlags="+".join( [ "NEGOTIATE_UNICODE", @@ -1408,10 +1546,14 @@ def GSS_Init_sec_context( "TARGET_TYPE_DOMAIN", "NEGOTIATE_EXTENDED_SESSIONSECURITY", "NEGOTIATE_TARGET_INFO", - "NEGOTIATE_VERSION", "NEGOTIATE_128", "NEGOTIATE_56", ] + + ( + ["NEGOTIATE_VERSION"] + if self.VARIANT >= NTLM_VARIANT.XP_OR_2003 + else [] + ) + ( [ "NEGOTIATE_KEY_EXCH", @@ -1455,54 +1597,79 @@ def GSS_Init_sec_context( return Context, tok, GSS_S_CONTINUE_NEEDED elif Context.state == self.STATE.CLI_SENT_NEGO: # Client: auth (token=challenge) - chall_tok = token + chall_tok = input_token if self.UPN is None or self.HASHNT is None: raise ValueError( "Must provide a 'UPN' and a 'HASHNT' or 'PASSWORD' when " "running in standalone !" ) + + from scapy.layers.kerberos import _parse_upn + + # Check token sanity if not chall_tok or NTLM_CHALLENGE not in chall_tok: log_runtime.debug("NTLMSSP: Unexpected token. Expected NTLM Challenge") return Context, None, GSS_S_DEFECTIVE_TOKEN - # Take a default token + + # Some information from the CHALLENGE are stored + try: + Context.ServerHostname = chall_tok.getAv(0x0001).Value + except IndexError: + pass + try: + Context.ServerDomain = chall_tok.getAv(0x0002).Value + except IndexError: + pass + try: + # the server SHOULD set the timestamp in the CHALLENGE_MESSAGE + ServerTimestamp = chall_tok.getAv(0x0007).Value + ServerTime = (ServerTimestamp / 1e7) - 11644473600 + + if abs(ServerTime - time.time()) >= NTLMSSP.NTLM_MaxLifetime: + log_runtime.warning( + "Server and Client times are off by more than 36h !" + ) + # We could error here, but we don't. + except IndexError: + pass + + # Initialize a default token tok = NTLM_AUTHENTICATE_V2( + VARIANT=self.VARIANT, NegotiateFlags=chall_tok.NegotiateFlags, ProductMajorVersion=10, ProductMinorVersion=0, ProductBuild=19041, ) tok.LmChallengeResponse = LMv2_RESPONSE() - from scapy.layers.kerberos import _parse_upn + # Populate the token + # 1. Set username try: tok.UserName, realm = _parse_upn(self.UPN) except ValueError: - tok.UserName, realm = self.UPN, None + tok.UserName, realm = self.UPN, Context.ServerDomain + + # 2. Set domain name if realm is None: - try: - tok.DomainName = chall_tok.getAv(0x0002).Value - except IndexError: - log_runtime.warning( - "No realm specified in UPN, nor provided by server" - ) - tok.DomainName = self.DOMAIN_NB_NAME.encode() + log_runtime.warning( + "No realm specified in UPN, nor provided by server." + ) + tok.DomainName = self.DOMAIN_FQDN else: tok.DomainName = realm - try: - tok.Workstation = Context.ServerHostname = chall_tok.getAv( - 0x0001 - ).Value # noqa: E501 - except IndexError: - tok.Workstation = "WIN" + + # 3. Set workstation name + tok.Workstation = self.COMPUTER_NB_NAME + + # 4. Create and calculate the ChallengeResponse + # 4.1 Build the payload cr = tok.NtChallengeResponse = NTLMv2_RESPONSE( ChallengeFromClient=os.urandom(8), ) - try: - # the server SHOULD set the timestamp in the CHALLENGE_MESSAGE - cr.TimeStamp = chall_tok.getAv(0x0007).Value - except IndexError: - cr.TimeStamp = int((time.time() + 11644473600) * 1e7) + cr.TimeStamp = int((time.time() + 11644473600) * 1e7) cr.AvPairs = ( + # Repeat AvPairs from the server chall_tok.TargetInfo[:-1] + ( [ @@ -1530,7 +1697,10 @@ def GSS_Init_sec_context( else [] ) + [ - AV_PAIR(AvId="MsvAvTargetName", Value="host/" + tok.Workstation), + AV_PAIR( + AvId="MsvAvTargetName", + Value=target_name or ("host/" + Context.ServerHostname), + ), AV_PAIR(AvId="MsvAvEOL"), ] ) @@ -1544,19 +1714,22 @@ def GSS_Init_sec_context( ]: if key in self.NTLM_VALUES: setattr(tok, key, self.NTLM_VALUES[key]) - # Compute the ResponseKeyNT + + # 4.2 Compute the ResponseKeyNT ResponseKeyNT = NTOWFv2( None, tok.UserName, tok.DomainName, HashNt=self.HASHNT, ) - # Compute the NTProofStr + + # 4.3 Compute the NTProofStr cr.NTProofStr = cr.computeNTProofStr( ResponseKeyNT, chall_tok.ServerChallenge, ) - # Compute the Session Key + + # 4.4 Compute the Session Key SessionBaseKey = NTLMv2_ComputeSessionBaseKey(ResponseKeyNT, cr.NTProofStr) KeyExchangeKey = SessionBaseKey # Only true for NTLMv2 if chall_tok.NegotiateFlags.NEGOTIATE_KEY_EXCH: @@ -1567,8 +1740,12 @@ def GSS_Init_sec_context( ) else: ExportedSessionKey = KeyExchangeKey + + # 4.5 Compute the MIC if self.USE_MIC: tok.compute_mic(ExportedSessionKey, Context.neg_tok, chall_tok) + + # 5. Perform key computations Context.ExportedSessionKey = ExportedSessionKey # [MS-SMB] 3.2.5.3 Context.SessionKey = Context.ExportedSessionKey @@ -1587,12 +1764,15 @@ def GSS_Init_sec_context( tok.NegotiateFlags, ExportedSessionKey, "Server" ) Context.RecvSealHandle = RC4Init(Context.RecvSealKey) + + # Update the state Context.state = self.STATE.CLI_SENT_AUTH + return Context, tok, GSS_S_COMPLETE elif Context.state == self.STATE.CLI_SENT_AUTH: - if token: + if input_token: # what is that? - status = GSS_S_DEFECTIVE_CREDENTIAL + status = GSS_S_DEFECTIVE_TOKEN else: status = GSS_S_COMPLETE return Context, None, status @@ -1602,7 +1782,7 @@ def GSS_Init_sec_context( def GSS_Accept_sec_context( self, Context: CONTEXT, - token=None, + input_token=None, req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): @@ -1610,14 +1790,16 @@ def GSS_Accept_sec_context( Context = self.CONTEXT(IsAcceptor=True, req_flags=req_flags) if Context.state == self.STATE.INIT: - # Server: challenge (token=negotiate) - nego_tok = token + # Server: challenge (input_token=negotiate) + nego_tok = input_token if not nego_tok or NTLM_NEGOTIATE not in nego_tok: log_runtime.debug("NTLMSSP: Unexpected token. Expected NTLM Negotiate") return Context, None, GSS_S_DEFECTIVE_TOKEN - # Take a default token + + # Build the challenge token currentTime = (time.time() + 11644473600) * 1e7 tok = NTLM_CHALLENGE( + VARIANT=self.VARIANT, ServerChallenge=self.SERVER_CHALLENGE or os.urandom(8), NegotiateFlags="+".join( [ @@ -1628,11 +1810,15 @@ def GSS_Accept_sec_context( "NEGOTIATE_EXTENDED_SESSIONSECURITY", "NEGOTIATE_TARGET_INFO", "TARGET_TYPE_DOMAIN", - "NEGOTIATE_VERSION", "NEGOTIATE_128", "NEGOTIATE_KEY_EXCH", "NEGOTIATE_56", ] + + ( + ["NEGOTIATE_VERSION"] + if self.VARIANT >= NTLM_VARIANT.XP_OR_2003 + else [] + ) + ( ["NEGOTIATE_SIGN"] if nego_tok.NegotiateFlags.NEGOTIATE_SIGN @@ -1696,12 +1882,17 @@ def GSS_Accept_sec_context( if ((x in self.NTLM_VALUES) or (i in avpairs)) and self.NTLM_VALUES.get(x, True) is not None ] + + # Store for next step Context.chall_tok = tok + + # Update the state Context.state = self.STATE.SRV_SENT_CHAL + return Context, tok, GSS_S_CONTINUE_NEEDED elif Context.state == self.STATE.SRV_SENT_CHAL: - # server: OK or challenge again (token=auth) - auth_tok = token + # server: OK or challenge again (input_token=auth) + auth_tok = input_token if not auth_tok or NTLM_AUTHENTICATE_V2 not in auth_tok: log_runtime.debug( @@ -1710,7 +1901,7 @@ def GSS_Accept_sec_context( return Context, None, GSS_S_DEFECTIVE_TOKEN if self.DO_NOT_CHECK_LOGIN: - # Just trust me bro + # Just trust me bro. Typically used in "guest" mode. return Context, None, GSS_S_COMPLETE # Compute the session key @@ -1719,12 +1910,12 @@ def GSS_Accept_sec_context( # [MS-NLMP] sect 3.2.5.1.2 KeyExchangeKey = SessionBaseKey # Only true for NTLMv2 if auth_tok.NegotiateFlags.NEGOTIATE_KEY_EXCH: - if not auth_tok.EncryptedRandomSessionKeyLen: + try: + EncryptedRandomSessionKey = auth_tok.EncryptedRandomSessionKey + except AttributeError: # No EncryptedRandomSessionKey. libcurl for instance # hmm. this looks bad EncryptedRandomSessionKey = b"\x00" * 16 - else: - EncryptedRandomSessionKey = auth_tok.EncryptedRandomSessionKey ExportedSessionKey = RC4K(KeyExchangeKey, EncryptedRandomSessionKey) else: ExportedSessionKey = KeyExchangeKey @@ -1732,6 +1923,19 @@ def GSS_Accept_sec_context( # [MS-SMB] 3.2.5.3 Context.SessionKey = Context.ExportedSessionKey + # Check the timestamp + try: + ClientTimestamp = auth_tok.NtChallengeResponse.getAv(0x0007).Value + ClientTime = (ClientTimestamp / 1e7) - 11644473600 + + if abs(ClientTime - time.time()) >= NTLMSSP.NTLM_MaxLifetime: + log_runtime.warning( + "Server and Client times are off by more than 36h !" + ) + # We could error here, but we don't. + except IndexError: + pass + # Check the channel bindings if chan_bindings != GSS_C_NO_CHANNEL_BINDINGS: try: @@ -1744,7 +1948,6 @@ def GSS_Accept_sec_context( # Uhoh, we required channel bindings return Context, None, GSS_S_BAD_BINDINGS - # Check the NTProofStr if Context.SessionKey: # Compute NTLM keys Context.SendSignKey = SIGNKEY( @@ -1761,6 +1964,8 @@ def GSS_Accept_sec_context( auth_tok.NegotiateFlags, ExportedSessionKey, "Client" ) Context.RecvSealHandle = RC4Init(Context.RecvSealKey) + + # Check the NTProofStr if self._checkLogin(Context, auth_tok): # Set negotiated flags if auth_tok.NegotiateFlags.NEGOTIATE_SIGN: @@ -1842,20 +2047,24 @@ def _getSessionBaseKey(self, Context, auth_tok): """ Function that returns the SessionBaseKey from the ntlm Authenticate. """ - if auth_tok.UserNameLen: + try: username = auth_tok.UserName - else: + except AttributeError: username = None - if auth_tok.DomainNameLen: + try: domain = auth_tok.DomainName - else: + except AttributeError: domain = "" if self.IDENTITIES and username in self.IDENTITIES: ResponseKeyNT = NTOWFv2( - None, username, domain, HashNt=self.IDENTITIES[username] + None, + username, + domain, + HashNt=self.IDENTITIES[username], ) return NTLMv2_ComputeSessionBaseKey( - ResponseKeyNT, auth_tok.NtChallengeResponse.NTProofStr + ResponseKeyNT, + auth_tok.NtChallengeResponse.NTProofStr, ) elif self.IDENTITIES: log_runtime.debug("NTLMSSP: Bad credentials for %s" % username) @@ -1868,17 +2077,20 @@ def _checkLogin(self, Context, auth_tok): Overwrite and return True to bypass. """ # Create the NTLM AUTH - if auth_tok.UserNameLen: + try: username = auth_tok.UserName - else: + except AttributeError: username = None - if auth_tok.DomainNameLen: + try: domain = auth_tok.DomainName - else: + except AttributeError: domain = "" if username in self.IDENTITIES: ResponseKeyNT = NTOWFv2( - None, username, domain, HashNt=self.IDENTITIES[username] + None, + username, + domain, + HashNt=self.IDENTITIES[username], ) NTProofStr = auth_tok.NtChallengeResponse.computeNTProofStr( ResponseKeyNT, @@ -1898,26 +2110,41 @@ class NTLMSSP_DOMAIN(NTLMSSP): mode: :param UPN: the UPN of the machine account to login for Netlogon. - :param HASHNT: the HASHNT of the machine account to use for Netlogon. - :param PASSWORD: the PASSWORD of the machine acconut to use for Netlogon. + :param HASHNT: the HASHNT of the machine account (use Netlogon secure channel). + :param ssp: a KerberosSSP to use (use Kerberos secure channel). + :param PASSWORD: the PASSWORD of the machine account to use for Netlogon. :param DC_IP: (optional) specify the IP of the DC. - Examples:: + Netlogon example:: >>> mySSP = NTLMSSP_DOMAIN( ... UPN="Server1@domain.local", ... HASHNT=bytes.fromhex("8846f7eaee8fb117ad06bdd830b7586c"), ... ) + + Kerberos example:: + + >>> mySSP = NTLMSSP_DOMAIN( + ... UPN="Server1@domain.local", + ... KEY=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, + ... key=bytes.fromhex( + ... "85abb9b61dc2fa49d4cc04317bbd108f8f79df28" + ... "239155ed7b144c5d2ebcf016" + ... ) + ... ), + ... ) """ - def __init__(self, UPN, *args, timeout=3, ssp=None, **kwargs): + def __init__(self, UPN=None, *args, timeout=3, ssp=None, **kwargs): from scapy.layers.kerberos import KerberosSSP - # UPN is mandatory - kwargs["UPN"] = UPN - # Either PASSWORD or HASHNT or ssp - if "HASHNT" not in kwargs and "PASSWORD" not in kwargs and ssp is None: + if ( + "HASHNT" not in kwargs + and "PASSWORD" not in kwargs + and "KEY" not in kwargs + and ssp is None + ): raise ValueError( "Must specify either 'HASHNT', 'PASSWORD' or " "provide a ssp=KerberosSSP()" @@ -1925,6 +2152,16 @@ def __init__(self, UPN, *args, timeout=3, ssp=None, **kwargs): elif ssp is not None and not isinstance(ssp, KerberosSSP): raise ValueError("'ssp' can only be None or a KerberosSSP !") + self.KEY = kwargs.pop("KEY", None) + self.PASSWORD = kwargs.get("PASSWORD", None) + + # UPN is mandatory + if UPN is None and ssp is not None and ssp.UPN: + UPN = ssp.UPN + elif UPN is None: + raise ValueError("Must specify a 'UPN' !") + kwargs["UPN"] = UPN + # Call parent super(NTLMSSP_DOMAIN, self).__init__( *args, @@ -1932,16 +2169,17 @@ def __init__(self, UPN, *args, timeout=3, ssp=None, **kwargs): ) # Treat specific parameters - self.DC_IP = kwargs.pop("DC_IP", None) - if self.DC_IP is None: - # Get DC_IP from dclocator + self.DC_FQDN = kwargs.pop("DC_FQDN", None) + if self.DC_FQDN is None: + # Get DC_FQDN from dclocator from scapy.layers.ldap import dclocator - self.DC_IP = dclocator( + dc = dclocator( self.DOMAIN_FQDN, timeout=timeout, debug=kwargs.get("debug", 0), - ).ip + ) + self.DC_FQDN = dc.samlogon.DnsHostName.decode().rstrip(".") # If logging in via Kerberos self.ssp = ssp @@ -1957,37 +2195,41 @@ def _getSessionBaseKey(self, Context, ntlm): # Import RPC stuff from scapy.layers.dcerpc import NDRUnion from scapy.layers.msrpce.msnrpc import ( - NetlogonClient, NETLOGON_SECURE_CHANNEL_METHOD, + NetlogonClient, ) from scapy.layers.msrpce.raw.ms_nrpc import ( + NETLOGON_LOGON_IDENTITY_INFO, NetrLogonSamLogonWithFlags_Request, - PNETLOGON_NETWORK_INFO, PNETLOGON_AUTHENTICATOR, - NETLOGON_LOGON_IDENTITY_INFO, - UNICODE_STRING, + PNETLOGON_NETWORK_INFO, STRING, + UNICODE_STRING, ) # Create NetlogonClient with PRIVACY client = NetlogonClient() - client.connect_and_bind(self.DC_IP) + client.connect(self.DC_FQDN) # Establish the Netlogon secure channel (this will bind) try: - if self.ssp is None: + if self.ssp is None and self.KEY is None: # Login via classic NetlogonSSP client.establish_secure_channel( mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3, - computername=self.COMPUTER_NB_NAME, - domainname=self.DOMAIN_NB_NAME, - HashNt=self.HASHNT, + UPN=f"{self.COMPUTER_NB_NAME}@{self.DOMAIN_NB_NAME}", + DC_FQDN=self.DC_FQDN, + HASHNT=self.HASHNT, ) else: # Login via KerberosSSP (Windows 2025) - # TODO client.establish_secure_channel( mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticateKerberos, + UPN=self.UPN, + DC_FQDN=self.DC_FQDN, + PASSWORD=self.PASSWORD, + KEY=self.KEY, + ssp=self.ssp, ) except ValueError: log_runtime.warning( diff --git a/scapy/layers/smb.py b/scapy/layers/smb.py index 676021e1d6b..541ab10c292 100644 --- a/scapy/layers/smb.py +++ b/scapy/layers/smb.py @@ -1001,10 +1001,11 @@ class NETLOGON_SAM_LOGON_RESPONSE_NT40(NETLOGON): 0x00000800: "SELECT_SECRET_DOMAIN_6", 0x00001000: "FULL_SECRET_DOMAIN_6", 0x00002000: "WS", - 0x00004000: "DS_8", - 0x00008000: "DS_9", - 0x00010000: "DS_10", # guess - 0x00020000: "DS_11", # guess + 0x00004000: "DS_8", # >=2008R2 + 0x00008000: "DS_9", # >=2012 + 0x00010000: "DS_10", # >=2016 + 0x00020000: "DS_11", # >=2019 + 0x00040000: "DS_12", # >=2025 0x20000000: "DNS_CONTROLLER", 0x40000000: "DNS_DOMAIN", 0x80000000: "DNS_FOREST", diff --git a/scapy/layers/smb2.py b/scapy/layers/smb2.py index 7d17e828375..3b4e650c9d7 100644 --- a/scapy/layers/smb2.py +++ b/scapy/layers/smb2.py @@ -4717,6 +4717,7 @@ def recv(self, x=None): pkt = self.queue.popleft() else: pkt = super(SMBStreamSocket, self).recv(x) + # If there are multiple SMB2_Header requests (aka. compounded), # take the first and store the rest in a queue. if pkt is not None and ( @@ -4725,14 +4726,17 @@ def recv(self, x=None): or SMB2_Compression_Transform_Header in pkt ): pkt = self.session.in_pkt(pkt) - pay = pkt[SMB2_Header].payload + smbh = pkt[SMB2_Header] + pay = smbh.payload while SMB2_Header in pay: pay = pay[SMB2_Header] + pay._decrypted = smbh._decrypted # Keep the _decrypted flag pay.underlayer.remove_payload() self.queue.append(pay) if not pay.NextCommand: break pay = pay.payload + # Verify the signature if required. # This happens here because we must have split compounded requests first. smbh = pkt.getlayer(SMB2_Header) diff --git a/scapy/layers/smbclient.py b/scapy/layers/smbclient.py index 360acbce824..68bdc30d4c8 100644 --- a/scapy/layers/smbclient.py +++ b/scapy/layers/smbclient.py @@ -447,7 +447,7 @@ def NEGOTIATED(self, ssp_blob=None): # Begin session establishment ssp_tuple = self.session.ssp.GSS_Init_sec_context( self.session.sspcontext, - token=ssp_blob, + input_token=ssp_blob, target_name="cifs/" + self.HOST if self.HOST else None, req_flags=( GSS_C_FLAGS.GSS_C_MUTUAL_FLAG @@ -476,8 +476,8 @@ def update_smbheader(self, pkt): @ATMT.condition(NEGOTIATED, prio=1) def should_send_session_setup_request(self, ssp_tuple): - _, _, negResult = ssp_tuple - if negResult not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: + _, _, status = ssp_tuple + if status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: raise ValueError("Internal error: the SSP completed with an error.") raise self.SENT_SESSION_REQUEST().action_parameters(ssp_tuple) @@ -487,8 +487,8 @@ def SENT_SESSION_REQUEST(self): @ATMT.action(should_send_session_setup_request) def send_setup_session_request(self, ssp_tuple): - self.session.sspcontext, token, negResult = ssp_tuple - if self.SMB2 and negResult == GSS_S_CONTINUE_NEEDED: + self.session.sspcontext, token, status = ssp_tuple + if self.SMB2 and status == GSS_S_CONTINUE_NEEDED: # New session: force 0 self.SessionId = 0 if self.SMB2 or self.EXTENDED_SECURITY: @@ -608,7 +608,7 @@ def AUTH_FAILED(self): def AUTHENTICATED(self, ssp_blob=None): self.session.sspcontext, _, status = self.session.ssp.GSS_Init_sec_context( self.session.sspcontext, - token=ssp_blob, + input_token=ssp_blob, target_name="cifs/" + self.HOST if self.HOST else None, ) if status != GSS_S_COMPLETE: @@ -1123,7 +1123,7 @@ def __init__( HashAes256Sha96: bytes = None, HashAes128Sha96: bytes = None, port: int = 445, - timeout: int = 2, + timeout: int = 5, debug: int = 0, ssp=None, ST=None, diff --git a/scapy/layers/smbserver.py b/scapy/layers/smbserver.py index ff20151c7bd..be1c68ee247 100644 --- a/scapy/layers/smbserver.py +++ b/scapy/layers/smbserver.py @@ -662,7 +662,8 @@ def RECEIVED_SETUP_ANDX_REQUEST(self): @ATMT.action(receive_setup_andx_request) def on_setup_andx_request(self, pkt, ssp_blob): self.session.sspcontext, tok, status = self.session.ssp.GSS_Accept_sec_context( - self.session.sspcontext, ssp_blob + self.session.sspcontext, + ssp_blob, ) self.update_smbheader(pkt) if SMB2_Session_Setup_Request in pkt: diff --git a/scapy/layers/spnego.py b/scapy/layers/spnego.py index 3afb73268ed..a37091313b3 100644 --- a/scapy/layers/spnego.py +++ b/scapy/layers/spnego.py @@ -16,13 +16,14 @@ `GSSAPI `_ """ +import os import struct from uuid import UUID from scapy.asn1.asn1 import ( - ASN1_OID, - ASN1_STRING, ASN1_Codecs, + ASN1_OID, + ASN1_GENERAL_STRING, ) from scapy.asn1.mib import conf # loads conf.mib from scapy.asn1fields import ( @@ -31,14 +32,14 @@ ASN1F_FLAGS, ASN1F_GENERAL_STRING, ASN1F_OID, + ASN1F_optional, ASN1F_PACKET, - ASN1F_SEQUENCE, ASN1F_SEQUENCE_OF, + ASN1F_SEQUENCE, + ASN1F_STRING_ENCAPS, ASN1F_STRING, - ASN1F_optional, ) from scapy.asn1packet import ASN1_Packet -from scapy.base_classes import Net from scapy.fields import ( FieldListField, LEIntEnumField, @@ -56,32 +57,34 @@ XStrFixedLenField, XStrLenField, ) +from scapy.error import log_runtime from scapy.packet import Packet, bind_layers from scapy.utils import ( valid_ip, valid_ip6, ) -from scapy.layers.inet6 import Net6 from scapy.layers.gssapi import ( - GSSAPI_BLOB, - GSSAPI_BLOB_SIGNATURE, + _GSSAPI_OIDS, + _GSSAPI_SIGNATURE_OIDS, GSS_C_FLAGS, GSS_C_NO_CHANNEL_BINDINGS, GSS_S_BAD_MECH, GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED, + GSS_S_FAILURE, GSS_S_FLAGS, + GSSAPI_BLOB_SIGNATURE, + GSSAPI_BLOB, GssChannelBindings, SSP, - _GSSAPI_OIDS, - _GSSAPI_SIGNATURE_OIDS, ) # SSP Providers from scapy.layers.kerberos import ( Kerberos, KerberosSSP, + _parse_spn, _parse_upn, ) from scapy.layers.ntlm import ( @@ -96,6 +99,7 @@ # Typing imports from typing import ( Dict, + List, Optional, Tuple, ) @@ -116,13 +120,14 @@ class SPNEGO_MechTypes(ASN1_Packet): class SPNEGO_MechListMIC(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_STRING("value", "") + ASN1_root = ASN1F_STRING_ENCAPS("value", "", GSSAPI_BLOB_SIGNATURE) _mechDissector = { "1.3.6.1.4.1.311.2.2.10": NTLM_Header, # NTLM "1.2.840.48018.1.2.2": Kerberos, # MS KRB5 - Microsoft Kerberos 5 "1.2.840.113554.1.2.2": Kerberos, # Kerberos 5 + "1.2.840.113554.1.2.2.3": Kerberos, # Kerberos 5 - User to User } @@ -134,13 +139,16 @@ def i2m(self, pkt, x): def m2i(self, pkt, s): dat, r = super(_SPNEGO_Token_Field, self).m2i(pkt, s) + types = None if isinstance(pkt.underlayer, SPNEGO_negTokenInit): types = pkt.underlayer.mechTypes elif isinstance(pkt.underlayer, SPNEGO_negTokenResp): types = [pkt.underlayer.supportedMech] if types and types[0] and types[0].oid.val in _mechDissector: return _mechDissector[types[0].oid.val](dat.val), r - return dat, r + else: + # Use heuristics + return GSSAPI_BLOB(dat.val), r class SPNEGO_Token(ASN1_Packet): @@ -208,7 +216,7 @@ class SPNEGO_negTokenResp(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE( ASN1F_optional( ASN1F_ENUMERATED( - "negResult", + "negState", 0, { 0: "accept-completed", @@ -262,6 +270,17 @@ class SPNEGO_negToken(ASN1_Packet): def mechListMIC(oids): """ Implementation of RFC 4178 - Appendix D. mechListMIC Computation + + NOTE: The documentation on mechListMIC isn't super clear, so note that: + + - The mechListMIC that the client sends is computed over the + list of mechanisms that it requests. + - the mechListMIC that the server sends is computed over the + list of mechanisms that the client requested. + + This also means that NegTokenInit2 added by [MS-SPNG] is NOT protected. + That's not necessarily an issue, since it was optional in most cases, + but it's something to keep in mind. """ return bytes(SPNEGO_MechTypes(mechTypes=oids)) @@ -528,105 +547,160 @@ class SPNEGOSSP(SSP): """ __slots__ = [ - "supported_ssps", - "force_supported_mechtypes", + "ssps", ] + auth_type = 0x09 class STATE(SSP.STATE): FIRST = 1 - CHANGESSP = 2 - NORMAL = 3 + SUBSEQUENT = 2 class CONTEXT(SSP.CONTEXT): __slots__ = [ - "supported_mechtypes", - "requested_mechtypes", "req_flags", - "negotiated_mechtype", + "ssps", + "other_mechtypes", + "sent_mechtypes", "first_choice", - "sub_context", + "require_mic", + "verified_mic", "ssp", + "ssp_context", + "ssp_mechtype", + "raw", ] def __init__( - self, supported_ssps, req_flags=None, force_supported_mechtypes=None + self, + ssps: List[SSP], + req_flags=None, ): self.state = SPNEGOSSP.STATE.FIRST - self.requested_mechtypes = None self.req_flags = req_flags - self.first_choice = True - self.negotiated_mechtype = None - self.sub_context = None + # Information used during negotiation + self.ssps = ssps + self.other_mechtypes = None # the mechtypes our peer requested + self.sent_mechtypes = None # the mechtypes we sent when acting as a client + self.first_choice = True # whether the SSP was the peer's first choice + self.require_mic = False # whether the mechListMIC is required or not + self.verified_mic = False # whether mechListMIC has been verified + # Information about the currently selected SSP self.ssp = None - if force_supported_mechtypes is None: - self.supported_mechtypes = [ - SPNEGO_MechType(oid=ASN1_OID(oid)) for oid in supported_ssps - ] - self.supported_mechtypes.sort( - key=lambda x: SPNEGOSSP._PREF_ORDER.index(x.oid.val) - ) - else: - self.supported_mechtypes = force_supported_mechtypes + self.ssp_context = None + self.ssp_mechtype = None + self.raw = False # fallback to raw SSP super(SPNEGOSSP.CONTEXT, self).__init__() + # This is the order Windows chooses + _PREF_ORDER = [ + "1.2.840.113554.1.2.2.3", # Kerberos 5 - User to User + "1.2.840.48018.1.2.2", # MS KRB5 + "1.2.840.113554.1.2.2", # Kerberos 5 + "1.3.6.1.4.1.311.2.2.30", # NEGOEX + "1.3.6.1.4.1.311.2.2.10", # NTLM + ] + + def get_supported_mechtypes(self): + """ + Return an ordered list of mechtypes that are still available. + """ + # 1. Build mech list + mechs = [] + for ssp in self.ssps: + mechs.extend(ssp.GSS_Inquire_names_for_mech()) + + # 2. Sort according to the preference order. + mechs.sort(key=lambda x: self._PREF_ORDER.index(x)) + + # 3. Return wrapped in MechType + return [SPNEGO_MechType(oid=ASN1_OID(oid)) for oid in mechs] + + def negotiate_ssp(self) -> None: + """ + Perform SSP negotiation. + + This updates our context and sets it with the first SSP that is + common to both client and server. This also applies rules from + [MS-SPNG] and RFC4178 to determine if mechListMIC is required. + """ + if self.other_mechtypes is None: + # We don't have any information about the peer's preferred SSPs. + # This typically happens on client side, when NegTokenInit2 isn't used. + self.ssp = self.ssps[0] + ssp_oid = self.ssp.GSS_Inquire_names_for_mech()[0] + else: + # Get first common SSP between us and our peer + other_oids = [x.oid.val for x in self.other_mechtypes] + try: + self.ssp, ssp_oid = next( + (ssp, requested_oid) + for requested_oid in other_oids + for ssp in self.ssps + if requested_oid in ssp.GSS_Inquire_names_for_mech() + ) + except StopIteration: + raise ValueError( + "Could not find a common SSP with the remote peer !" + ) + + # Check whether the selected SSP was the one preferred by the client + self.first_choice = ssp_oid == other_oids[0] + + # Check whether mechListMIC is mandatory for this exchange + if not self.first_choice: + # RFC4178 rules for mechListMIC: mandatory if not the first choice. + self.require_mic = True + elif ssp_oid == "1.3.6.1.4.1.311.2.2.10" and self.ssp.SupportsMechListMIC(): + # [MS-SPNG] note 8: "If NTLM authentication is most preferred by + # the client and the server, and the client includes a MIC in + # AUTHENTICATE_MESSAGE, then the mechListMIC field becomes + # mandatory" + self.require_mic = True + + # Get the associated ssp dissection class and mechtype + self.ssp_mechtype = SPNEGO_MechType(oid=ASN1_OID(ssp_oid)) + + # Reset the ssp context + self.ssp_context = None + # Passthrough attributes and functions def clifailure(self): - self.sub_context.clifailure() + if self.ssp_context is not None: + self.ssp_context.clifailure() def __getattr__(self, attr): try: return object.__getattribute__(self, attr) except AttributeError: - return getattr(self.sub_context, attr) + return getattr(self.ssp_context, attr) def __setattr__(self, attr, val): try: return object.__setattr__(self, attr, val) except AttributeError: - return setattr(self.sub_context, attr, val) + return setattr(self.ssp_context, attr, val) # Passthrough the flags property @property def flags(self): - if self.sub_context: - return self.sub_context.flags + if self.ssp_context: + return self.ssp_context.flags return GSS_C_FLAGS(0) @flags.setter def flags(self, x): - if not self.sub_context: + if not self.ssp_context: return - self.sub_context.flags = x + self.ssp_context.flags = x def __repr__(self): - return "SPNEGOSSP[%s]" % repr(self.sub_context) - - _MECH_ALIASES = { - # Kerberos has 2 ssps - "1.2.840.48018.1.2.2": "1.2.840.113554.1.2.2", - "1.2.840.113554.1.2.2": "1.2.840.48018.1.2.2", - } - - # This is the order Windows chooses. We mimic it for plausibility - _PREF_ORDER = [ - "1.2.840.48018.1.2.2", # MS KRB5 - "1.2.840.113554.1.2.2", # Kerberos 5 - "1.3.6.1.4.1.311.2.2.30", # NEGOEX - "1.3.6.1.4.1.311.2.2.10", # NTLM - ] + return "SPNEGOSSP[%s]" % repr(self.ssp_context) - def __init__(self, ssps, **kwargs): - self.supported_ssps = {x.oid: x for x in ssps} - # Apply MechTypes aliases - for ssp in ssps: - if ssp.oid in self._MECH_ALIASES: - self.supported_ssps[self._MECH_ALIASES[ssp.oid]] = self.supported_ssps[ - ssp.oid - ] - self.force_supported_mechtypes = kwargs.pop("force_supported_mechtypes", None) + def __init__(self, ssps: List[SSP], **kwargs): + self.ssps = ssps super(SPNEGOSSP, self).__init__(**kwargs) @classmethod @@ -640,8 +714,11 @@ def from_cli_arguments( HashAes128Sha96: bytes = None, kerberos_required: bool = False, ST=None, + TGT=None, KEY=None, + ccache: str = None, debug: int = 0, + use_krb5ccname: bool = False, ): """ Initialize a SPNEGOSSP from a list of many arguments. @@ -655,8 +732,12 @@ def from_cli_arguments( :param HashAes256Sha96: (bytes) if provided, used for auth (Kerberos) :param HashAes128Sha96: (bytes) if provided, used for auth (Kerberos) :param ST: if provided, the service ticket to use (Kerberos) + :param TGT: if provided, the TGT to use (Kerberos) :param KEY: if ST provided, the session key associated to the ticket (Kerberos). - Else, the user secret key. + This can be either for the ST or TGT. Else, the user secret key. + :param ccache: (str) if provided, a path to a CCACHE (Kerberos) + :param use_krb5ccname: (bool) if true, the KRB5CCNAME environment variable will + be used if available. """ kerberos = True hostname = None @@ -664,11 +745,9 @@ def from_cli_arguments( if ":" in target: if not valid_ip6(target): hostname = target - target = str(Net6(target)) else: if not valid_ip(target): hostname = target - target = str(Net(target)) # Check UPN try: @@ -680,6 +759,10 @@ def from_cli_arguments( # not a UPN: NTLM only kerberos = False + # If we're asked, check the environment for KRB5CCNAME + if use_krb5ccname and ccache is None and "KRB5CCNAME" in os.environ: + ccache = os.environ["KRB5CCNAME"] + # Do we need to ask the password? if all( x is None @@ -689,6 +772,7 @@ def from_cli_arguments( HashNt, HashAes256Sha96, HashAes128Sha96, + ccache, ] ): # yes. @@ -700,7 +784,44 @@ def from_cli_arguments( # Kerberos if kerberos and hostname: # Get ticket if we don't already have one. - if ST is None: + if ST is None and TGT is None and ccache is not None: + # In this case, load the KerberosSSP from ccache + from scapy.modules.ticketer import Ticketer + + # Import into a Ticketer object + t = Ticketer() + t.open_ccache(ccache) + + # Look for the ticket that we'll use. We chose: + # - either a ST if the SPN matches our target + # - else a TGT if we got nothing better + tgts = [] + for i, (tkt, key, upn, spn) in enumerate(t.iter_tickets()): + spn, _ = _parse_spn(spn) + spn_host = spn.split("/")[-1] + # Check that it's for the correct user + if upn.lower() == UPN.lower(): + # Check that it's either a TGT or a ST to the correct service + if spn.lower().startswith("krbtgt/"): + # TGT. Keep it, and see if we don't have a better ST. + tgts.append(t.ssp(i)) + elif hostname.lower() == spn_host.lower(): + # ST. We're done ! + ssps.append(t.ssp(i)) + break + else: + # No ST found + if tgts: + # Using a TGT ! + ssps.append(tgts[0]) + else: + # Nothing found + t.show() + raise ValueError( + f"Could not find a ticket for {upn}, either a " + f"TGT or towards {hostname}" + ) + elif ST is None and TGT is None: # In this case, KEY is supposed to be the user's key. from scapy.libs.rfc3961 import Key, EncryptionType @@ -734,6 +855,7 @@ def from_cli_arguments( KerberosSSP( UPN=UPN, ST=ST, + TGT=TGT, KEY=KEY, debug=debug, ) @@ -748,68 +870,33 @@ def from_cli_arguments( if not kerberos_required: if HashNt is None and password is not None: HashNt = MD4le(password) - ssps.append(NTLMSSP(UPN=UPN, HASHNT=HashNt)) + if HashNt is not None: + ssps.append(NTLMSSP(UPN=UPN, HASHNT=HashNt)) + + if not ssps: + raise ValueError("Unexpected case ! Please report.") # Build the SSP return cls(ssps) - def _extract_gssapi(self, Context, x): - status, otherMIC, rawToken = None, None, False - # Extract values from GSSAPI - if isinstance(x, GSSAPI_BLOB): - x = x.innerToken - if isinstance(x, SPNEGO_negToken): - x = x.token - if hasattr(x, "mechTypes"): - Context.requested_mechtypes = x.mechTypes - Context.negotiated_mechtype = None - if hasattr(x, "supportedMech") and x.supportedMech is not None: - Context.negotiated_mechtype = x.supportedMech - if hasattr(x, "mechListMIC") and x.mechListMIC: - otherMIC = GSSAPI_BLOB_SIGNATURE(x.mechListMIC.value.val) - if hasattr(x, "_mechListMIC") and x._mechListMIC: - otherMIC = GSSAPI_BLOB_SIGNATURE(x._mechListMIC.value.val) - if hasattr(x, "negResult"): - status = x.negResult - try: - x = x.mechToken - except AttributeError: - try: - x = x.responseToken - except AttributeError: - # No GSSAPI wrapper (windows fallback). Remember this for answer - rawToken = True - if isinstance(x, SPNEGO_Token): - x = x.value - if Context.requested_mechtypes: - try: - cls = _mechDissector[ - ( - Context.negotiated_mechtype or Context.requested_mechtypes[0] - ).oid.val # noqa: E501 - ] - except KeyError: - cls = conf.raw_layer - if isinstance(x, ASN1_STRING): - x = cls(x.val) - elif isinstance(x, conf.raw_layer): - x = cls(x.load) - return x, status, otherMIC, rawToken - def NegTokenInit2(self): """ Server-Initiation of GSSAPI/SPNEGO. See [MS-SPNG] sect 3.2.5.2 """ - Context = self.CONTEXT( - self.supported_ssps, - force_supported_mechtypes=self.force_supported_mechtypes, - ) + Context = SPNEGOSSP.CONTEXT(list(self.ssps)) return ( Context, GSSAPI_BLOB( innerToken=SPNEGO_negToken( - token=SPNEGO_negTokenInit(mechTypes=Context.supported_mechtypes) + token=SPNEGO_negTokenInit( + mechTypes=Context.get_supported_mechtypes(), + negHints=SPNEGO_negHints( + hintName=ASN1_GENERAL_STRING( + "not_defined_in_RFC4178@please_ignore" + ), + ), + ) ) ), ) @@ -830,320 +917,374 @@ def NegTokenInit2(self): def GSS_WrapEx(self, Context, *args, **kwargs): # Passthrough - return Context.ssp.GSS_WrapEx(Context.sub_context, *args, **kwargs) + return Context.ssp.GSS_WrapEx(Context.ssp_context, *args, **kwargs) def GSS_UnwrapEx(self, Context, *args, **kwargs): # Passthrough - return Context.ssp.GSS_UnwrapEx(Context.sub_context, *args, **kwargs) + return Context.ssp.GSS_UnwrapEx(Context.ssp_context, *args, **kwargs) def GSS_GetMICEx(self, Context, *args, **kwargs): # Passthrough - return Context.ssp.GSS_GetMICEx(Context.sub_context, *args, **kwargs) + return Context.ssp.GSS_GetMICEx(Context.ssp_context, *args, **kwargs) def GSS_VerifyMICEx(self, Context, *args, **kwargs): # Passthrough - return Context.ssp.GSS_VerifyMICEx(Context.sub_context, *args, **kwargs) + return Context.ssp.GSS_VerifyMICEx(Context.ssp_context, *args, **kwargs) def LegsAmount(self, Context: CONTEXT): return 4 - def _common_spnego_handler( + def MapStatusToNegState(self, status: int) -> int: + """ + Map a GSSAPI return code to SPNEGO negState codes + """ + if status == GSS_S_COMPLETE: + return 0 # accept_completed + elif status == GSS_S_CONTINUE_NEEDED: + return 1 # accept_incomplete + else: + return 2 # reject + + def GuessOtherMechtypes(self, Context: CONTEXT, input_token): + """ + Guesses the mechtype of the peer when the "raw" fallback is used. + """ + if isinstance(input_token, NTLM_Header): + Context.other_mechtypes = [ + SPNEGO_MechType(oid=ASN1_OID("1.3.6.1.4.1.311.2.2.10")) + ] + elif isinstance(input_token, Kerberos): + Context.other_mechtypes = [ + SPNEGO_MechType(oid=ASN1_OID("1.2.840.48018.1.2.2")) + ] + else: + Context.other_mechtypes = [] + + def GSS_Init_sec_context( self, - Context, - IsClient, - token=None, + Context: CONTEXT, + input_token=None, target_name: Optional[str] = None, - req_flags=None, + req_flags: Optional[GSS_C_FLAGS] = None, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): - """ - Common code shared across both GSS_sec_Init_Context and GSS_sec_Accept_Context - """ if Context is None: # New Context Context = SPNEGOSSP.CONTEXT( - self.supported_ssps, + list(self.ssps), req_flags=req_flags, - force_supported_mechtypes=self.force_supported_mechtypes, ) - if IsClient: - Context.requested_mechtypes = Context.supported_mechtypes - # Extract values from GSSAPI token - status, MIC, otherMIC, rawToken = 0, None, None, False - if token: - token, status, otherMIC, rawToken = self._extract_gssapi(Context, token) + input_token_inner = None + negState = None + + # Extract values from GSSAPI token, if present + if input_token is not None: + if isinstance(input_token, GSSAPI_BLOB): + input_token = input_token.innerToken + if isinstance(input_token, SPNEGO_negToken): + input_token = input_token.token + if isinstance(input_token, SPNEGO_negTokenInit): + # We are handling a NegTokenInit2 request ! + # Populate context with values from the server's request + Context.other_mechtypes = input_token.mechTypes + elif isinstance(input_token, SPNEGO_negTokenResp): + # Extract token and state from the client request + if input_token.responseToken is not None: + input_token_inner = input_token.responseToken.value + if input_token.negState is not None: + negState = input_token.negState + else: + # The blob is a raw token. We aren't using SPNEGO here. + Context.raw = True + input_token_inner = input_token + self.GuessOtherMechtypes(Context, input_token) - # If we don't have a SSP already negotiated, check for requested and available - # SSPs and find a common one. + # Perform SSP negotiation if Context.ssp is None: - if Context.negotiated_mechtype is None: - if Context.requested_mechtypes: - # Find a common SSP - try: - Context.negotiated_mechtype = next( - x - for x in Context.requested_mechtypes - if x in Context.supported_mechtypes - ) - except StopIteration: - # no common mechanisms - raise ValueError("No common SSP mechanisms !") - # Check whether the selected SSP was the one preferred by the client - if ( - Context.negotiated_mechtype != Context.requested_mechtypes[0] - and token - ): - Context.first_choice = False - # No SSPs were requested. Use the first available SSP we know. - elif Context.supported_mechtypes: - Context.negotiated_mechtype = Context.supported_mechtypes[0] - else: - raise ValueError("Can't figure out what SSP to use") - # Set Context.ssp to the object matching the chosen SSP type. - Context.ssp = self.supported_ssps[Context.negotiated_mechtype.oid.val] + try: + Context.negotiate_ssp() + except ValueError as ex: + # Couldn't find common SSP + log_runtime.warning("SPNEGOSSP: %s" % ex) + return Context, None, GSS_S_BAD_MECH + + # Call inner-SSP + Context.ssp_context, output_token_inner, status = ( + Context.ssp.GSS_Init_sec_context( + Context.ssp_context, + input_token=input_token_inner, + target_name=target_name, + req_flags=Context.req_flags, + chan_bindings=chan_bindings, + ) + ) - if not Context.first_choice: - # The currently provided token is not for this SSP ! - # Typically a client opportunistically starts with Kerberos, including - # its APREQ, and we want to use NTLM. We add one round trip - Context.state = SPNEGOSSP.STATE.FIRST - Context.first_choice = True # reset to not come here again. - tok, status = None, GSS_S_CONTINUE_NEEDED - else: - # The currently provided token is for this SSP ! - # Pass it to the sub ssp, with its own context - if IsClient: - Context.sub_context, tok, status = Context.ssp.GSS_Init_sec_context( - Context.sub_context, - token=token, + if negState == 2 or status not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: + # SSP failed. Remove it from the list of SSPs we're currently running + Context.ssps.remove(Context.ssp) + log_runtime.warning( + "SPNEGOSSP: %s failed. Retrying with next in queue." % repr(Context.ssp) + ) + + if Context.ssps: + # We have other SSPs remaining. Retry using another one. + Context.ssp = None + return self.GSS_Init_sec_context( + Context, + None, # No input for retry. target_name=target_name, - req_flags=Context.req_flags, + req_flags=req_flags, chan_bindings=chan_bindings, ) else: - Context.sub_context, tok, status = Context.ssp.GSS_Accept_sec_context( - Context.sub_context, - token=token, - req_flags=Context.req_flags, - chan_bindings=chan_bindings, - ) - # Check whether client or server says the specified mechanism is not valid - if status == GSS_S_BAD_MECH: - # Mechanism is not usable. Typically the Kerberos SPN is wrong - to_remove = [Context.negotiated_mechtype.oid.val] - # If there's an alias (for the multiple kerberos oids, also include it) - if Context.negotiated_mechtype.oid.val in SPNEGOSSP._MECH_ALIASES: - to_remove.append( - SPNEGOSSP._MECH_ALIASES[Context.negotiated_mechtype.oid.val] - ) - # Drop those unusable mechanisms from the supported list - for x in list(Context.supported_mechtypes): - if x.oid.val in to_remove: - Context.supported_mechtypes.remove(x) - break - # Re-calculate negotiated mechtype - try: - Context.negotiated_mechtype = next( - x - for x in Context.requested_mechtypes - if x in Context.supported_mechtypes - ) - except StopIteration: - # no common mechanisms - raise ValueError("No common SSP mechanisms after GSS_S_BAD_MECH !") - # Start again. - Context.state = SPNEGOSSP.STATE.CHANGESSP - Context.ssp = None # Reset the SSP - Context.sub_context = None # Reset the SSP context - if IsClient: - # Call ourselves again for the client to generate a token - return self._common_spnego_handler( - Context, - IsClient=True, - token=None, - req_flags=req_flags, - chan_bindings=chan_bindings, - ) - else: - # Return nothing but the supported SSP list - tok, status = None, GSS_S_CONTINUE_NEEDED - - if rawToken: - # No GSSAPI wrapper (fallback) - return Context, tok, status + # We don't have anything left + return Context, None, status + + # Raw processing ends here. + if Context.raw: + return Context, output_token_inner, status + + # Verify MIC if present. + if status == GSS_S_COMPLETE and input_token and input_token.mechListMIC: + # NOTE: the mechListMIC that the server sends is computed over the list of + # mechanisms that the **client requested**. + Context.ssp.VerifyMechListMIC( + Context.ssp_context, + input_token.mechListMIC.value, + mechListMIC(Context.sent_mechtypes), + ) + Context.verified_mic = True - # Client success - if IsClient and tok is None and status == GSS_S_COMPLETE: + if negState == 0 and status == GSS_S_COMPLETE: + # We are done. return Context, None, status + elif Context.state == SPNEGOSSP.STATE.FIRST: + # First freeze the list of available mechtypes on the first message + Context.sent_mechtypes = Context.get_supported_mechtypes() - # Map GSSAPI codes to SPNEGO - if status == GSS_S_COMPLETE: - negResult = 0 # accept_completed - elif status == GSS_S_CONTINUE_NEEDED: - negResult = 1 # accept_incomplete - else: - negResult = 2 # reject - - # GSSAPI-MIC - if Context.ssp and Context.ssp.canMechListMIC(Context.sub_context): - # The documentation on mechListMIC wasn't clear, so note that: - # - The mechListMIC that the client sends is computed over the - # list of mechanisms that it requests. - # - the mechListMIC that the server sends is computed over the - # list of mechanisms that the client requested. - # Yes, this does indeed mean that NegTokenInit2 added by [MS-SPNG] - # is NOT protected. That's not necessarily an issue, since it was - # optional in most cases, but it's something to keep in mind. - if otherMIC is not None: - # Check the received MIC if any - if IsClient: # from server - Context.ssp.verifyMechListMIC( - Context, - otherMIC, - mechListMIC(Context.supported_mechtypes), - ) - else: # from client - Context.ssp.verifyMechListMIC( - Context, - otherMIC, - mechListMIC(Context.requested_mechtypes), - ) - # Then build our own MIC - if IsClient: # client - if negResult == 0: - # Include MIC for the last packet. We could add a check - # here to only send the MIC when required (when preferred ssp - # isn't chosen) - MIC = Context.ssp.getMechListMIC( - Context, - mechListMIC(Context.supported_mechtypes), - ) - else: # server - MIC = Context.ssp.getMechListMIC( - Context, - mechListMIC(Context.requested_mechtypes), + # Now build the token + spnego_tok = GSSAPI_BLOB( + innerToken=SPNEGO_negToken( + token=SPNEGO_negTokenInit(mechTypes=Context.sent_mechtypes) ) + ) - if IsClient: - if Context.state == SPNEGOSSP.STATE.FIRST: - # First client token - spnego_tok = SPNEGO_negToken( - token=SPNEGO_negTokenInit(mechTypes=Context.supported_mechtypes) - ) - if tok: - spnego_tok.token.mechToken = SPNEGO_Token(value=tok) - else: - # Subsequent client tokens - spnego_tok = SPNEGO_negToken( # GSSAPI_BLOB is stripped - token=SPNEGO_negTokenResp( - supportedMech=None, - negResult=None, - ) + # Add the output token if provided + if output_token_inner is not None: + spnego_tok.innerToken.token.mechToken = SPNEGO_Token( + value=output_token_inner, ) - if tok: - spnego_tok.token.responseToken = SPNEGO_Token(value=tok) - if Context.state == SPNEGOSSP.STATE.CHANGESSP: - # On renegotiation, include the negResult and chosen mechanism - spnego_tok.token.negResult = negResult - spnego_tok.token.supportedMech = Context.negotiated_mechtype - else: - spnego_tok = SPNEGO_negToken( # GSSAPI_BLOB is stripped + elif Context.state == SPNEGOSSP.STATE.SUBSEQUENT: + # Build subsequent client tokens: without the list of supported mechtypes + # NOTE: GSSAPI_BLOB is stripped. + spnego_tok = SPNEGO_negToken( token=SPNEGO_negTokenResp( supportedMech=None, - negResult=negResult, + negState=None, ) ) - if Context.state in [SPNEGOSSP.STATE.FIRST, SPNEGOSSP.STATE.CHANGESSP]: - # Include the supportedMech list if this is the first thing we do - # or a renegotiation. - spnego_tok.token.supportedMech = Context.negotiated_mechtype - if tok: - spnego_tok.token.responseToken = SPNEGO_Token(value=tok) - # Apply MIC if available - if MIC: - spnego_tok.token.mechListMIC = SPNEGO_MechListMIC( - value=ASN1_STRING(MIC), - ) - if ( - IsClient and Context.state == SPNEGOSSP.STATE.FIRST - ): # Client: after the first packet, specifying 'SPNEGO' is implicit. - # Always implicit for the server. - spnego_tok = GSSAPI_BLOB(innerToken=spnego_tok) - # Not the first token anymore - Context.state = SPNEGOSSP.STATE.NORMAL + + # Add the MIC if required and the exchange is finished. + if status == GSS_S_COMPLETE and Context.require_mic: + spnego_tok.token.mechListMIC = SPNEGO_MechListMIC( + value=Context.ssp.GetMechListMIC( + Context.ssp_context, + mechListMIC(Context.sent_mechtypes), + ), + ) + + # If we still haven't verified the MIC, we aren't done. + if not Context.verified_mic: + status = GSS_S_CONTINUE_NEEDED + + # Add the output token if provided + if output_token_inner: + spnego_tok.token.responseToken = SPNEGO_Token( + value=output_token_inner, + ) + + # Update the state + Context.state = SPNEGOSSP.STATE.SUBSEQUENT + return Context, spnego_tok, status - def GSS_Init_sec_context( + def GSS_Accept_sec_context( self, Context: CONTEXT, - token=None, - target_name: Optional[str] = None, - req_flags: Optional[GSS_C_FLAGS] = None, + input_token=None, + req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, ): - return self._common_spnego_handler( - Context, - True, - token=token, - target_name=target_name, - req_flags=req_flags, - chan_bindings=chan_bindings, + if Context is None: + # New Context + Context = SPNEGOSSP.CONTEXT( + list(self.ssps), + req_flags=req_flags, + ) + + input_token_inner = None + _mechListMIC = None + + # Extract values from GSSAPI token + if isinstance(input_token, GSSAPI_BLOB): + input_token = input_token.innerToken + if isinstance(input_token, SPNEGO_negToken): + input_token = input_token.token + if isinstance(input_token, SPNEGO_negTokenInit): + # Populate context with values from the client's request + if input_token.mechTypes: + Context.other_mechtypes = input_token.mechTypes + if input_token.mechToken: + input_token_inner = input_token.mechToken.value + _mechListMIC = input_token.mechListMIC or input_token._mechListMIC + elif isinstance(input_token, SPNEGO_negTokenResp): + if input_token.responseToken: + input_token_inner = input_token.responseToken.value + _mechListMIC = input_token.mechListMIC + else: + # The blob is a raw token. We aren't using SPNEGO here. + Context.raw = True + input_token_inner = input_token + self.GuessOtherMechtypes(Context, input_token) + + if Context.other_mechtypes is None: + # At this point, we should have already gotten the mechtypes from a current + # or former request. + return Context, None, GSS_S_FAILURE + + # Perform SSP negotiation + if Context.ssp is None: + try: + Context.negotiate_ssp() + except ValueError as ex: + # Couldn't find common SSP + log_runtime.warning("SPNEGOSSP: %s" % ex) + return Context, None, GSS_S_FAILURE + + output_token_inner = None + status = GSS_S_CONTINUE_NEEDED + + # If we didn't pick the client's first choice, the token we were passed + # isn't usable. + if not Context.first_choice: + # Typically a client opportunistically starts with Kerberos, including + # its APREQ, and we want to use NTLM. Here we add one round trip + Context.first_choice = True # Do not enter here again. + else: + # Send it to the negotiated SSP + Context.ssp_context, output_token_inner, status = ( + Context.ssp.GSS_Accept_sec_context( + Context.ssp_context, + input_token=input_token_inner, + req_flags=Context.req_flags, + chan_bindings=chan_bindings, + ) + ) + + # Verify MIC if context succeeded + if status == GSS_S_COMPLETE and _mechListMIC: + # NOTE: the mechListMIC that the client sends is computed over the + # **list of mechanisms that it requests**. + if Context.ssp.SupportsMechListMIC(): + # We need to check we support checking the MIC. The only case where + # this is needed is NTLM in guest mode: the client will send a mic + # but we don't check it... + Context.ssp.VerifyMechListMIC( + Context.ssp_context, + _mechListMIC.value, + mechListMIC(Context.other_mechtypes), + ) + Context.verified_mic = True + Context.require_mic = True + + # Raw processing ends here. + if Context.raw: + return Context, output_token_inner, status + + # 0. Build the template response token + spnego_tok = SPNEGO_negToken( + token=SPNEGO_negTokenResp( + supportedMech=None, + ) ) + if Context.state == SPNEGOSSP.STATE.FIRST: + # Include the supportedMech list if this is the first message we send + # or a renegotiation. + spnego_tok.token.supportedMech = Context.ssp_mechtype - def GSS_Accept_sec_context( + # Add the output token if provided + if output_token_inner: + spnego_tok.token.responseToken = SPNEGO_Token(value=output_token_inner) + + # Update the state + Context.state = SPNEGOSSP.STATE.SUBSEQUENT + + # Add the MIC if required and the exchange is finished. + if status == GSS_S_COMPLETE and Context.require_mic: + spnego_tok.token.mechListMIC = SPNEGO_MechListMIC( + value=Context.ssp.GetMechListMIC( + Context.ssp_context, + mechListMIC(Context.other_mechtypes), + ), + ) + + # If we still haven't verified the MIC, we aren't done. + if not Context.verified_mic: + status = GSS_S_CONTINUE_NEEDED + + # Set negState + spnego_tok.token.negState = self.MapStatusToNegState(status) + + return Context, spnego_tok, status + + def GSS_Passive( self, Context: CONTEXT, - token=None, - req_flags: Optional[GSS_S_FLAGS] = GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS, - chan_bindings: GssChannelBindings = GSS_C_NO_CHANNEL_BINDINGS, + input_token=None, + req_flags=None, ): - return self._common_spnego_handler( - Context, - False, - token=token, - req_flags=req_flags, - chan_bindings=chan_bindings, - ) - - def GSS_Passive(self, Context: CONTEXT, token=None, req_flags=None): if Context is None: # New Context - Context = SPNEGOSSP.CONTEXT(self.supported_ssps) + Context = SPNEGOSSP.CONTEXT(list(self.ssps)) Context.passive = True - # Extraction - token, status, _, rawToken = self._extract_gssapi(Context, token) + input_token_inner = None - if token is None and status == GSS_S_COMPLETE: - return Context, None - - # Just get the negotiated SSP - if Context.negotiated_mechtype: - mechtype = Context.negotiated_mechtype - elif Context.requested_mechtypes: - mechtype = Context.requested_mechtypes[0] - elif rawToken and Context.supported_mechtypes: - mechtype = Context.supported_mechtypes[0] - else: - return None, GSS_S_BAD_MECH - try: - ssp = self.supported_ssps[mechtype.oid.val] - except KeyError: - return None, GSS_S_BAD_MECH - - if Context.ssp is not None: - # Detect resets - if Context.ssp != ssp: - Context.ssp = ssp - Context.sub_context = None + # Extract values from GSSAPI token + if isinstance(input_token, GSSAPI_BLOB): + input_token = input_token.innerToken + if isinstance(input_token, SPNEGO_negToken): + input_token = input_token.token + if isinstance(input_token, SPNEGO_negTokenInit): + if input_token.mechTypes is not None: + Context.other_mechtypes = input_token.mechTypes + if input_token.mechToken: + input_token_inner = input_token.mechToken.value + elif isinstance(input_token, SPNEGO_negTokenResp): + if input_token.supportedMech is not None: + Context.other_mechtypes = [input_token.supportedMech] + if input_token.responseToken: + input_token_inner = input_token.responseToken.value else: - Context.ssp = ssp + # Raw. + input_token_inner = input_token + + if Context.other_mechtypes is None: + self.GuessOtherMechtypes(Context, input_token) + + # Uninitialized OR allowed mechtypes have changed + if Context.ssp is None or Context.ssp_mechtype not in Context.other_mechtypes: + try: + Context.negotiate_ssp() + except ValueError: + # Couldn't find common SSP + return Context, GSS_S_FAILURE # Passthrough - Context.sub_context, status = Context.ssp.GSS_Passive( - Context.sub_context, - token, + Context.ssp_context, status = Context.ssp.GSS_Passive( + Context.ssp_context, + input_token_inner, req_flags=req_flags, ) @@ -1151,8 +1292,8 @@ def GSS_Passive(self, Context: CONTEXT, token=None, req_flags=None): def GSS_Passive_set_Direction(self, Context: CONTEXT, IsAcceptor=False): Context.ssp.GSS_Passive_set_Direction( - Context.sub_context, IsAcceptor=IsAcceptor + Context.ssp_context, IsAcceptor=IsAcceptor ) def MaximumSignatureLength(self, Context: CONTEXT): - return Context.ssp.MaximumSignatureLength(Context.sub_context) + return Context.ssp.MaximumSignatureLength(Context.ssp_context) diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py index b38f52ca073..4788c3d2a88 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -4,69 +4,156 @@ # Copyright (C) 2008 Arnaud Ebalard # # 2015, 2016, 2017 Maxence Tury +# 2022-2025 Gabriel Potter -""" -High-level methods for PKI objects (X.509 certificates, CRLs, asymmetric keys). -Supports both RSA and ECDSA objects. +r""" +High-level methods for PKI objects (X.509 certificates, CRLs, CSR, Keys, CMS). +Supported keys include RSA, ECDSA and EdDSA. The classes below are wrappers for the ASN.1 objects defined in x509.py. + +Example 1: Certificate & Private key +____________________________________ + For instance, here is what you could do in order to modify the subject public key info of a 'cert' and then resign it with whatever 'key':: - from scapy.layers.tls.cert import * - cert = Cert("cert.der") - k = PrivKeyRSA() # generate a private key - cert.setSubjectPublicKeyFromPrivateKey(k) - cert.resignWith(k) - cert.export("newcert.pem") - k.export("mykey.pem") + >>> from scapy.layers.tls.cert import * + >>> cert = Cert("cert.der") + >>> k = PrivKeyRSA() # generate a private key + >>> cert.setSubjectPublicKeyFromPrivateKey(k) + >>> cert.resignWith(k) + >>> cert.export("newcert.pem") + >>> k.export("mykey.pem") One could also edit arguments like the serial number, as such:: - from scapy.layers.tls.cert import * - c = Cert("mycert.pem") - c.tbsCertificate.serialNumber = 0x4B1D - k = PrivKey("mykey.pem") # import an existing private key - c.resignWith(k) - c.export("newcert.pem") + >>> from scapy.layers.tls.cert import * + >>> c = Cert("mycert.pem") + >>> c.tbsCertificate.serialNumber = 0x4B1D + >>> k = PrivKey("mykey.pem") # import an existing private key + >>> c.resignWith(k) + >>> c.export("newcert.pem") To export the public key of a private key:: - k = PrivKey("mykey.pem") - k.pubkey.export("mypubkey.pem") + >>> k = PrivKey("mykey.pem") + >>> k.pubkey.export("mypubkey.pem") + +Example 2: CertList and CertTree +________________________________ + +Load a .pem file that contains multiple certificates:: + + >>> l = CertList("ca_chain.pem") + >>> l.show() + 0000 [X.509 Cert Subject:/C=FR/OU=Scapy Test PKI/CN=Scapy Test CA...] + 0001 [X.509 Cert Subject:/C=FR/OU=Scapy Test PKI/CN=Scapy Test Client...] + +Use 'CertTree' to organize the certificates in a tree:: + + >>> tree = CertTree("ca_chain.pem") # or tree = CertTree(l) + >>> tree.show() + /C=Ulaanbaatar/OU=Scapy Test PKI/CN=Scapy Test CA [Self Signed] + /C=FR/OU=Scapy Test PKI/CN=Scapy Test Client [Not Self Signed] + +Example 3: Certificate Signing Request (CSR) +____________________________________________ + +Scapy's :py:class:`~scapy.layers.tls.cert.CSR` class supports both PKCS#10 and CMC +formats. + +Load and display a CSR:: + + >>> csr = CSR("cert.req") + >>> csr + [CSR Format: CMC, Subject:/O=TestOrg/CN=TestCN, Verified: True] + >>> csr.certReq.show() + ###[ PKCS10_CertificationRequest ]### + \certificationRequestInfo\ + |###[ PKCS10_CertificationRequestInfo ]### + | version = 0x0 + | | | value = + [...] + +Get its public key and verify its signature:: + + >>> csr.pubkey + + >>> csr.verifySelf() + True No need for obnoxious openssl tweaking anymore. :) """ import base64 +import enum import os import time +import warnings from scapy.config import conf, crypto_validator +from scapy.compat import Self from scapy.error import warning from scapy.utils import binrepr -from scapy.asn1.asn1 import ASN1_BIT_STRING +from scapy.asn1.asn1 import ( + ASN1_BIT_STRING, + ASN1_NULL, + ASN1_OID, + ASN1_STRING, +) from scapy.asn1.mib import hash_by_oid +from scapy.packet import Packet from scapy.layers.x509 import ( + CMS_CertificateChoices, + CMS_ContentInfo, + CMS_EncapsulatedContentInfo, + CMS_IssuerAndSerialNumber, + CMS_RevocationInfoChoice, + CMS_SignedAttrsForSignature, + CMS_SignedData, + CMS_SignerInfo, + CMS_SubjectKeyIdentifier, ECDSAPrivateKey_OpenSSL, ECDSAPrivateKey, ECDSAPublicKey, - EdDSAPublicKey, EdDSAPrivateKey, + EdDSAPublicKey, + PKCS10_CertificationRequest, RSAPrivateKey_OpenSSL, RSAPrivateKey, RSAPublicKey, + X509_AlgorithmIdentifier, + X509_Attribute, + X509_AttributeValue, X509_Cert, X509_CRL, X509_SubjectPublicKeyInfo, ) -from scapy.layers.tls.crypto.pkcs1 import pkcs_os2ip, _get_hash, \ - _EncryptAndVerifyRSA, _DecryptAndSignRSA -from scapy.compat import raw, bytes_encode +from scapy.layers.tls.crypto.pkcs1 import ( + _DecryptAndSignRSA, + _EncryptAndVerifyRSA, + _get_hash, + pkcs_os2ip, +) +from scapy.compat import bytes_encode + +# Typing imports +from typing import ( + List, + Optional, + Union, +) if conf.crypto_valid: from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa, ec, x25519 @@ -89,21 +176,23 @@ # loading huge file when importing a cert _MAX_KEY_SIZE = 50 * 1024 _MAX_CERT_SIZE = 50 * 1024 -_MAX_CRL_SIZE = 10 * 1024 * 1024 # some are that big +_MAX_CRL_SIZE = 10 * 1024 * 1024 # some are that big +_MAX_CSR_SIZE = 50 * 1024 ##################################################################### # Some helpers ##################################################################### + @conf.commands.register def der2pem(der_string, obj="UNKNOWN"): """Convert DER octet string to PEM format (with optional header)""" # Encode a byte string in PEM format. Header advertises type. pem_string = "-----BEGIN %s-----\n" % obj base64_string = base64.b64encode(der_string).decode() - chunks = [base64_string[i:i + 64] for i in range(0, len(base64_string), 64)] # noqa: E501 - pem_string += '\n'.join(chunks) + chunks = [base64_string[i : i + 64] for i in range(0, len(base64_string), 64)] + pem_string += "\n".join(chunks) pem_string += "\n-----END %s-----\n" % obj return pem_string @@ -164,7 +253,7 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None): raise Exception(error_msg) obj_path = bytes_encode(obj_path) - if (b'\x00' not in obj_path) and os.path.isfile(obj_path): + if (b"\x00" not in obj_path) and os.path.isfile(obj_path): _size = os.path.getsize(obj_path) if _size > obj_max_size: raise Exception(error_msg) @@ -181,7 +270,7 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None): frmt = "PEM" pem = _raw der_list = split_pem(pem) - der = b''.join(map(pem2der, der_list)) + der = b"".join(map(pem2der, der_list)) else: frmt = "DER" der = _raw @@ -200,12 +289,14 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None): # Public Keys # ############### + class _PubKeyFactory(_PKIObjMaker): """ Metaclass for PubKey creation. It casts the appropriate class on the fly, then fills in the appropriate attributes with import_from_asn1pkt() submethod. """ + def __call__(cls, key_path=None, cryptography_obj=None): # This allows to import cryptography objects directly if cryptography_obj is not None: @@ -275,12 +366,18 @@ class PubKey(metaclass=_PubKeyFactory): """ def verifyCert(self, cert): - """ Verifies either a Cert or an X509_Cert. """ + """Verifies either a Cert or an X509_Cert.""" + h = _get_cert_sig_hashname(cert) tbsCert = cert.tbsCertificate - sigAlg = tbsCert.signature - h = hash_by_oid[sigAlg.algorithm.val] - sigVal = raw(cert.signatureValue) - return self.verify(raw(tbsCert), sigVal, h=h, t='pkcs') + sigVal = bytes(cert.signatureValue) + return self.verify(bytes(tbsCert), sigVal, h=h, t="pkcs") + + def verifyCsr(self, csr): + """Verifies a CSR.""" + h = _get_csr_sig_hashname(csr) + certReqInfo = csr.certReq.certificationRequestInfo + sigVal = bytes(csr.certReq.signature) + return self.verify(bytes(certReqInfo), sigVal, h=h, t="pkcs") @property def pem(self): @@ -315,12 +412,20 @@ def export(self, filename, fmt=None): elif fmt == "PEM": return f.write(self.pem.encode()) + @crypto_validator + def verify(self, msg, sig, h="sha256", **kwargs): + """ + Verify signed data. + """ + raise NotImplementedError + class PubKeyRSA(PubKey, _EncryptAndVerifyRSA): """ Wrapper for RSA keys based on _EncryptAndVerifyRSA from crypto/pkcs1.py Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None): pubExp = pubExp or 65537 @@ -374,8 +479,7 @@ def encrypt(self, msg, t="pkcs", h="sha256", mgf=None, L=None): return _EncryptAndVerifyRSA.encrypt(self, msg, t=t, h=h, mgf=mgf, L=L) def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): - return _EncryptAndVerifyRSA.verify( - self, msg, sig, t=t, h=h, mgf=mgf, L=L) + return _EncryptAndVerifyRSA.verify(self, msg, sig, t=t, h=h, mgf=mgf, L=L) class PubKeyECDSA(PubKey): @@ -383,6 +487,7 @@ class PubKeyECDSA(PubKey): Wrapper for ECDSA keys based on the cryptography library. Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or ec.SECP256R1 @@ -415,6 +520,7 @@ class PubKeyEdDSA(PubKey): Wrapper for EdDSA keys based on the cryptography library. Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or x25519.X25519PrivateKey @@ -445,12 +551,14 @@ def verify(self, msg, sig, **kwargs): # Private Keys # ################ + class _PrivKeyFactory(_PKIObjMaker): """ Metaclass for PrivKey creation. It casts the appropriate class on the fly, then fills in the appropriate attributes with import_from_asn1pkt() submethod. """ + def __call__(cls, key_path=None, cryptography_obj=None): """ key_path may be the path to either: @@ -472,11 +580,14 @@ def __call__(cls, key_path=None, cryptography_obj=None): if cryptography_obj is not None: # We (stupidly) need to go through the whole import process because RSA # does more than just importing the cryptography objects... - obj = _PKIObj("DER", cryptography_obj.private_bytes( - encoding=serialization.Encoding.DER, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption() - )) + obj = _PKIObj( + "DER", + cryptography_obj.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ), + ) else: # Load from file obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE) @@ -518,8 +629,10 @@ def __call__(cls, key_path=None, cryptography_obj=None): class _Raw_ASN1_BIT_STRING(ASN1_BIT_STRING): """A ASN1_BIT_STRING that ignores BER encoding""" + def __bytes__(self): return self.val_readable + __str__ = __bytes__ @@ -546,7 +659,7 @@ def signTBSCert(self, tbsCert, h="sha256"): """ sigAlg = tbsCert.signature h = h or hash_by_oid[sigAlg.algorithm.val] - sigVal = self.sign(raw(tbsCert), h=h, t='pkcs') + sigVal = self.sign(bytes(tbsCert), h=h, t="pkcs") c = X509_Cert() c.tbsCertificate = tbsCert c.signatureAlgorithm = sigAlg @@ -554,16 +667,16 @@ def signTBSCert(self, tbsCert, h="sha256"): return c def resignCert(self, cert): - """ Rewrite the signature of either a Cert or an X509_Cert. """ + """Rewrite the signature of either a Cert or an X509_Cert.""" return self.signTBSCert(cert.tbsCertificate, h=None) def verifyCert(self, cert): - """ Verifies either a Cert or an X509_Cert. """ - tbsCert = cert.tbsCertificate - sigAlg = tbsCert.signature - h = hash_by_oid[sigAlg.algorithm.val] - sigVal = raw(cert.signatureValue) - return self.verify(raw(tbsCert), sigVal, h=h, t='pkcs') + """Verifies either a Cert or an X509_Cert.""" + return self.pubkey.verifyCert(cert) + + def verifyCsr(self, cert): + """Verifies either a CSR.""" + return self.pubkey.verifyCsr(cert) @property def pem(self): @@ -574,7 +687,7 @@ def der(self): return self.key.private_bytes( encoding=serialization.Encoding.DER, format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption() + encryption_algorithm=serialization.NoEncryption(), ) def export(self, filename, fmt=None): @@ -592,19 +705,50 @@ def export(self, filename, fmt=None): elif fmt == "PEM": return f.write(self.pem.encode()) + @crypto_validator + def sign(self, data, h="sha256", **kwargs): + """ + Sign data. + """ + raise NotImplementedError + + @crypto_validator + def verify(self, msg, sig, h="sha256", **kwargs): + """ + Verify signed data. + """ + raise NotImplementedError + class PrivKeyRSA(PrivKey, _DecryptAndSignRSA): """ Wrapper for RSA keys based on _DecryptAndSignRSA from crypto/pkcs1.py Use the 'key' attribute to access original object. """ + @crypto_validator - def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None, - prime1=None, prime2=None, coefficient=None, - exponent1=None, exponent2=None, privExp=None): + def fill_and_store( + self, + modulus=None, + modulusLen=None, + pubExp=None, + prime1=None, + prime2=None, + coefficient=None, + exponent1=None, + exponent2=None, + privExp=None, + ): pubExp = pubExp or 65537 - if None in [modulus, prime1, prime2, coefficient, privExp, - exponent1, exponent2]: + if None in [ + modulus, + prime1, + prime2, + coefficient, + privExp, + exponent1, + exponent2, + ]: # note that the library requires every parameter # in order to call RSAPrivateNumbers(...) # if one of these is missing, we generate a whole new key @@ -627,10 +771,15 @@ def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None, if modulusLen and real_modulusLen != modulusLen: warning("modulus and modulusLen do not match!") pubNum = rsa.RSAPublicNumbers(n=modulus, e=pubExp) - privNum = rsa.RSAPrivateNumbers(p=prime1, q=prime2, - dmp1=exponent1, dmq1=exponent2, - iqmp=coefficient, d=privExp, - public_numbers=pubNum) + privNum = rsa.RSAPrivateNumbers( + p=prime1, + q=prime2, + dmp1=exponent1, + dmq1=exponent2, + iqmp=coefficient, + d=privExp, + public_numbers=pubNum, + ) self.key = privNum.private_key(default_backend()) pubkey = self.key.public_key() @@ -653,10 +802,16 @@ def import_from_asn1pkt(self, privkey): exponent1 = privkey.exponent1.val exponent2 = privkey.exponent2.val coefficient = privkey.coefficient.val - self.fill_and_store(modulus=modulus, pubExp=pubExp, - privExp=privExp, prime1=prime1, prime2=prime2, - exponent1=exponent1, exponent2=exponent2, - coefficient=coefficient) + self.fill_and_store( + modulus=modulus, + pubExp=pubExp, + privExp=privExp, + prime1=prime1, + prime2=prime2, + exponent1=exponent1, + exponent2=exponent2, + coefficient=coefficient, + ) def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): return self.pubkey.verify( @@ -677,6 +832,7 @@ class PrivKeyECDSA(PrivKey): Wrapper for ECDSA keys based on SigningKey from ecdsa library. Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or ec.SECP256R1 @@ -686,8 +842,9 @@ def fill_and_store(self, curve=None): @crypto_validator def import_from_asn1pkt(self, privkey): - self.key = serialization.load_der_private_key(raw(privkey), None, - backend=default_backend()) # noqa: E501 + self.key = serialization.load_der_private_key( + bytes(privkey), None, backend=default_backend() + ) self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key()) self.marker = "EC PRIVATE KEY" @@ -705,6 +862,7 @@ class PrivKeyEdDSA(PrivKey): Wrapper for EdDSA keys Use the 'key' attribute to access original object. """ + @crypto_validator def fill_and_store(self, curve=None): curve = curve or x25519.X25519PrivateKey @@ -714,8 +872,9 @@ def fill_and_store(self, curve=None): @crypto_validator def import_from_asn1pkt(self, privkey): - self.key = serialization.load_der_private_key(raw(privkey), None, - backend=default_backend()) # noqa: E501 + self.key = serialization.load_der_private_key( + bytes(privkey), None, backend=default_backend() + ) self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key()) self.marker = "PRIVATE KEY" @@ -732,21 +891,25 @@ def sign(self, data, **kwargs): # Certificates # ################ + class _CertMaker(_PKIObjMaker): """ Metaclass for Cert creation. It is not necessary as it was for the keys, but we reuse the model instead of creating redundant constructors. """ + def __call__(cls, cert_path=None, cryptography_obj=None): # This allows to import cryptography objects directly if cryptography_obj is not None: - obj = _PKIObj("DER", cryptography_obj.public_bytes( - encoding=serialization.Encoding.DER, - )) + obj = _PKIObj( + "DER", + cryptography_obj.public_bytes( + encoding=serialization.Encoding.DER, + ), + ) else: # Load from file - obj = _PKIObjMaker.__call__(cls, cert_path, - _MAX_CERT_SIZE, "CERTIFICATE") + obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CERT_SIZE, "CERTIFICATE") obj.__class__ = Cert obj.marker = "CERTIFICATE" try: @@ -759,6 +922,24 @@ def __call__(cls, cert_path=None, cryptography_obj=None): return obj +def _get_cert_sig_hashname(cert): + """ + Return the hash associated with the signature algorithm of a certificate. + """ + tbsCert = cert.tbsCertificate + sigAlg = tbsCert.signature + return hash_by_oid[sigAlg.algorithm.val] + + +def _get_csr_sig_hashname(csr): + """ + Return the hash associated with the signature algorithm of a CSR. + """ + certReq = csr.certReq + sigAlg = certReq.signatureAlgorithm + return hash_by_oid[sigAlg.algorithm.val] + + class Cert(metaclass=_CertMaker): """ Wrapper for the X509_Cert from layers/x509.py. @@ -771,7 +952,6 @@ def import_from_asn1pkt(self, cert): self.x509Cert = cert tbsCert = cert.tbsCertificate - self.tbsCertificate = tbsCert if tbsCert.version: self.version = tbsCert.version.val + 1 @@ -801,7 +981,7 @@ def import_from_asn1pkt(self, cert): raise Exception(error_msg) self.notAfter_str_simple = time.strftime("%x", self.notAfter) - self.pubKey = PubKey(raw(tbsCert.subjectPublicKeyInfo)) + self.pubkey = PubKey(bytes(tbsCert.subjectPublicKeyInfo)) if tbsCert.extensions: for extn in tbsCert.extensions: @@ -816,10 +996,10 @@ def import_from_asn1pkt(self, cert): elif extn.extnID.oidname == "authorityKeyIdentifier": self.authorityKeyID = extn.extnValue.keyIdentifier.val - self.signatureValue = raw(cert.signatureValue) + self.signatureValue = bytes(cert.signatureValue) self.signatureLen = len(self.signatureValue) - def isIssuerCert(self, other): + def isIssuer(self, other): """ True if 'other' issued 'self', i.e.: - self.issuer == other.subject @@ -827,7 +1007,10 @@ def isIssuerCert(self, other): """ if self.issuer_hash != other.subject_hash: return False - return other.pubKey.verifyCert(self) + return other.pubkey.verifyCert(self) + + def isIssuerCert(self, other): + return self.isIssuer(other) def isSelfSigned(self): """ @@ -836,24 +1019,21 @@ def isSelfSigned(self): - the signature of the certificate is valid. """ if self.issuer_hash == self.subject_hash: - return self.isIssuerCert(self) + return self.isIssuer(self) return False def encrypt(self, msg, t="pkcs", h="sha256", mgf=None, L=None): # no ECDSA *encryption* support, hence only RSA specific keywords here - return self.pubKey.encrypt(msg, t=t, h=h, mgf=mgf, L=L) + return self.pubkey.encrypt(msg, t=t, h=h, mgf=mgf, L=L) def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): - return self.pubKey.verify(msg, sig, t=t, h=h, mgf=mgf, L=L) + return self.pubkey.verify(msg, sig, t=t, h=h, mgf=mgf, L=L) def getSignatureHash(self): """ - Return the hash used by the 'signatureAlgorithm' + Return the hash cryptography object used by the 'signatureAlgorithm' """ - tbsCert = self.tbsCertificate - sigAlg = tbsCert.signature - h = hash_by_oid[sigAlg.algorithm.val] - return _get_hash(h) + return _get_hash(_get_cert_sig_hashname(self)) def setSubjectPublicKeyFromPrivateKey(self, key): """ @@ -901,17 +1081,17 @@ def remainingDays(self, now=None): now = time.localtime() elif isinstance(now, str): try: - if '/' in now: - now = time.strptime(now, '%m/%d/%y') + if "/" in now: + now = time.strptime(now, "%m/%d/%y") else: - now = time.strptime(now, '%b %d %H:%M:%S %Y %Z') + now = time.strptime(now, "%b %d %H:%M:%S %Y %Z") except Exception: - warning("Bad time string provided, will use localtime() instead.") # noqa: E501 + warning("Bad time string provided, will use localtime() instead.") now = time.localtime() now = time.mktime(now) nft = time.mktime(self.notAfter) - diff = (nft - now) / (24. * 3600) + diff = (nft - now) / (24.0 * 3600) return diff def isRevoked(self, crl_list): @@ -931,14 +1111,20 @@ def isRevoked(self, crl_list): Cert. Otherwise, the issuers are simply compared. """ for c in crl_list: - if (self.authorityKeyID is not None and - c.authorityKeyID is not None and - self.authorityKeyID == c.authorityKeyID): + if ( + self.authorityKeyID is not None + and c.authorityKeyID is not None + and self.authorityKeyID == c.authorityKeyID + ): return self.serial in (x[0] for x in c.revoked_cert_serials) elif self.issuer == c.issuer: return self.serial in (x[0] for x in c.revoked_cert_serials) return False + @property + def tbsCertificate(self): + return self.x509Cert.tbsCertificate + @property def pem(self): return der2pem(self.der, self.marker) @@ -947,6 +1133,21 @@ def pem(self): def der(self): return bytes(self.x509Cert) + @property + def pubKey(self): + warnings.warn( + "Cert.pubKey is deprecated and will be removed in a future version. " + "Use Cert.pubkey", + DeprecationWarning, + ) + return self.pubkey + + def __eq__(self, other): + return self.der == other.der + + def __hash__(self): + return hash(self.der) + def export(self, filename, fmt=None): """ Export certificate in 'fmt' format (DER or PEM) to file 'filename' @@ -969,18 +1170,23 @@ def show(self): print("Validity: %s to %s" % (self.notBefore_str, self.notAfter_str)) def __repr__(self): - return "[X.509 Cert. Subject:%s, Issuer:%s]" % (self.subject_str, self.issuer_str) # noqa: E501 + return "[X.509 Cert. Subject:%s, Issuer:%s]" % ( + self.subject_str, + self.issuer_str, + ) ################################ # Certificate Revocation Lists # ################################ + class _CRLMaker(_PKIObjMaker): """ Metaclass for CRL creation. It is not necessary as it was for the keys, but we reuse the model instead of creating redundant constructors. """ + def __call__(cls, cert_path): obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CRL_SIZE, "X509 CRL") obj.__class__ = CRL @@ -1004,7 +1210,7 @@ def import_from_asn1pkt(self, crl): self.x509CRL = crl tbsCertList = crl.tbsCertList - self.tbsCertList = raw(tbsCertList) + self.tbsCertList = bytes(tbsCertList) if tbsCertList.version: self.version = tbsCertList.version.val + 1 @@ -1057,18 +1263,18 @@ def import_from_asn1pkt(self, crl): revoked.append((serial, date)) self.revoked_cert_serials = revoked - self.signatureValue = raw(crl.signatureValue) + self.signatureValue = bytes(crl.signatureValue) self.signatureLen = len(self.signatureValue) - def isIssuerCert(self, other): + def isIssuer(self, other): # This is exactly the same thing as in Cert method. if self.issuer_hash != other.subject_hash: return False - return other.pubKey.verifyCert(self) + return other.pubkey.verifyCert(self) def verify(self, anchors): # Return True iff the CRL is signed by one of the provided anchors. - return any(self.isIssuerCert(a) for a in anchors) + return any(self.isIssuer(a) for a in anchors) def show(self): print("Version: %d" % self.version) @@ -1078,140 +1284,632 @@ def show(self): print("nextUpdate: %s" % self.nextUpdate_str) +############################### +# Certificate Signing Request # +############################### + + +class _CSRMaker(_PKIObjMaker): + """ + Metaclass for CSR creation. It is not necessary as it was for the keys, + but we reuse the model instead of creating redundant constructors. + """ + + def __call__(cls, cert_path): + obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CSR_SIZE) + obj.__class__ = CSR + try: + # PKCS#10 format + csr = PKCS10_CertificationRequest(obj._der) + obj.marker = "NEW CERTIFICATE REQUEST" + obj.fmt = CSR.FORMAT.PKCS10 + except Exception: + try: + # CMC format + csr = CMS_ContentInfo(obj._der) + obj.marker = "NEW CERTIFICATE REQUEST" + obj.fmt = CSR.FORMAT.CMC + except Exception: + raise Exception("Unable to import CSR") + + obj.import_from_asn1pkt(csr) + return obj + + +class CSR(metaclass=_CSRMaker): + """ + Wrapper for the CSR formats. + This can handle both PKCS#10 and CMC formats. + """ + + class FORMAT(enum.Enum): + """ + The format used by the CSR. + """ + + PKCS10 = "PKCS#10" + CMC = "CMC" + + def import_from_asn1pkt(self, csr): + self.csr = csr + certReqInfo = self.certReq.certificationRequestInfo + + # Subject + self.subject = certReqInfo.get_subject() + self.subject_str = certReqInfo.get_subject_str() + self.subject_hash = hash(self.subject_str) + + # pubkey + self.pubkey = PubKey(bytes(certReqInfo.subjectPublicKeyInfo)) + + # Get the "subjectKeyIdentifier" from the "extensionRequest" attribute + try: + extReq = next( + x.values[0].value + for x in certReqInfo.attributes + if x.type.val == "1.2.840.113549.1.9.14" # extKeyUsage + ) + self.sid = next( + x.extnValue.keyIdentifier + for x in extReq.extensions + if x.extnID.val == "2.5.29.14" # subjectKeyIdentifier + ) + except StopIteration: + self.sid = None + + @property + def certReq(self): + csr = self.csr + + if self.fmt == CSR.FORMAT.PKCS10: + return csr + elif self.fmt == CSR.FORMAT.CMC: + if ( + csr.contentType.oidname != "id-signedData" + or csr.content.encapContentInfo.eContentType.oidname != "id-cct-PKIData" + ): + raise ValueError("Invalid CMC wrapping !") + req = csr.content.encapContentInfo.eContent.reqSequence[0] + return req.request.certificationRequest + else: + raise ValueError("Invalid CSR format !") + + @property + def pem(self): + return der2pem(self.der, self.marker) + + @property + def der(self): + return bytes(self.csr) + + def __eq__(self, other): + return self.der == other.der + + def __hash__(self): + return hash(self.der) + + def isIssuer(self, other): + return other.sid == self.sid + + def isSelfSigned(self): + return True + + def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None): + return self.pubkey.verify(msg, sig, t=t, h=h, mgf=mgf, L=L) + + def export(self, filename, fmt=None): + """ + Export certificate in 'fmt' format (DER or PEM) to file 'filename' + """ + if fmt is None: + if filename.endswith(".pem"): + fmt = "PEM" + else: + fmt = "DER" + with open(filename, "wb") as f: + if fmt == "DER": + return f.write(self.der) + elif fmt == "PEM": + return f.write(self.pem.encode()) + + def show(self): + certReqInfo = self.certReq.certificationRequestInfo + + print("Subject: " + self.subject_str) + print("Attributes:") + for attr in certReqInfo.attributes: + print(" - %s" % attr.type.oidname) + + def verifySelf(self) -> bool: + """ + Verify the signatures of the CSR + """ + if self.fmt == self.FORMAT.CMC: + try: + cms_engine = CMS_Engine([self]) + cms_engine.verify(self.csr) + return self.pubkey.verifyCsr(self) + except ValueError: + return False + elif self.fmt == self.FORMAT.PKCS10: + return self.pubkey.verifyCsr(self) + else: + return False + + def __repr__(self): + return "[CSR Format: %s, Subject:%s, Verified: %s]" % ( + self.fmt.value, + self.subject_str, + self.verifySelf(), + ) + + +#################### +# Certificate list # +#################### + + +class CertList(list): + """ + An object that can store a list of Cert objects, load them and export them + into DER/PEM format. + """ + + def __init__( + self, + certList: Union[Self, List[Cert], List[CSR], Cert, str], + ): + """ + Construct a list of certificates/CRLs to be used as list of ROOT certificates. + """ + # Parse the certificate list / CA + if isinstance(certList, str): + # It's a path. First get the _PKIObj + obj = _PKIObjMaker.__call__( + CertList, certList, _MAX_CERT_SIZE, "CERTIFICATE" + ) + + # Then parse the der until there's nothing left + certList = [] + payload = obj._der + while payload: + cert = X509_Cert(payload) + if conf.raw_layer in cert.payload: + payload = cert.payload.load + else: + payload = None + cert.remove_payload() + certList.append(Cert(cert)) + + self.frmt = obj.frmt + elif isinstance(certList, Cert): + certList = [certList] + self.frmt = "PEM" + else: + self.frmt = "PEM" + + super(CertList, self).__init__(certList) + + def findCertBySid(self, sid): + """ + Find a certificate in the list by SubjectIDentifier. + """ + for cert in self: + if isinstance(cert, Cert) and isinstance(sid, CMS_IssuerAndSerialNumber): + if cert.issuer == sid.get_issuer(): + return cert + elif isinstance(cert, CSR) and isinstance(sid, CMS_SubjectKeyIdentifier): + if cert.sid == sid.sid: + return cert + raise KeyError("Certificate not found !") + + def export(self, filename, fmt=None): + """ + Export a list of certificates 'fmt' format (DER or PEM) to file 'filename' + """ + if fmt is None: + if filename.endswith(".pem"): + fmt = "PEM" + else: + fmt = "DER" + with open(filename, "wb") as f: + if fmt == "DER": + return f.write(self.der) + elif fmt == "PEM": + return f.write(self.pem.encode()) + + @property + def der(self): + return b"".join(x.der for x in self) + + @property + def pem(self): + return "".join(x.pem for x in self) + + def __repr__(self): + return "" % (len(self),) + + def show(self): + for i, c in enumerate(self): + print(conf.color_theme.id(i, fmt="%04i"), end=" ") + print(repr(c)) + + ###################### # Certificate chains # ###################### -class Chain(list): + +class CertTree(CertList): """ - Basically, an enhanced array of Cert. + An extension to CertList that additionally has a list of ROOT CAs + that are trusted. + + Example:: + + >>> tree = CertTree("ca_chain.pem") + >>> tree.show() + /CN=DOMAIN-DC1-CA/dc=DOMAIN [Self Signed] + /CN=Administrator/dc=DOMAIN [Not Self Signed] """ - def __init__(self, certList, cert0=None): + __slots__ = ["frmt", "rootCAs"] + + def __init__( + self, + certList: Union[List[Cert], CertList, str], + rootCAs: Union[List[Cert], CertList, Cert, str, None] = None, + ): """ - Construct a chain of certificates starting with a self-signed - certificate (or any certificate submitted by the user) - and following issuer/subject matching and signature validity. - If there is exactly one chain to be constructed, it will be, - but if there are multiple potential chains, there is no guarantee - that the retained one will be the longest one. - As Cert and CRL classes both share an isIssuerCert() method, - the trailing element of a Chain may alternatively be a CRL. + Construct a chain of certificates that follows issuer/subject matching and + respects signature validity. Note that we do not check AKID/{SKID/issuer/serial} matching, nor the presence of keyCertSign in keyUsage extension (if present). + + :param certList: a list of Cert/CRL objects (or path to PEM/DER file containing + multiple certs/CRL) to try to chain. + :param rootCAs: (optional) a list of certificates to trust. If not provided, + trusts any self-signed certificates from the certList. """ - list.__init__(self, ()) - if cert0: - self.append(cert0) + # Parse the certificate list + certList = CertList(certList) + + # Find the ROOT CAs if store isn't specified + if not rootCAs: + # Build cert store. + self.rootCAs = CertList([x for x in certList if x.isSelfSigned()]) + # And remove those certs from the list + for cert in self.rootCAs: + certList.remove(cert) else: - for root_candidate in certList: - if root_candidate.isSelfSigned(): - self.append(root_candidate) - certList.remove(root_candidate) - break - - if len(self) > 0: - while certList: - tmp_len = len(self) - for c in certList: - if c.isIssuerCert(self[-1]): - self.append(c) - certList.remove(c) - break - if len(self) == tmp_len: - # no new certificate appended to self - break - - def verifyChain(self, anchors, untrusted=None): + # Store cert store. + self.rootCAs = CertList(rootCAs) + # And remove those certs from the list if present (remove dups) + for cert in self.rootCAs: + if cert in certList: + certList.remove(cert) + + # Append our root CAs to the certList + certList.extend(self.rootCAs) + + # Super instantiate + super(CertTree, self).__init__(certList) + + @property + def tree(self): """ - Perform verification of certificate chains for that certificate. - A list of anchors is required. The certificates in the optional - untrusted list may be used as additional elements to the final chain. - On par with chain instantiation, only one chain constructed with the - untrusted candidates will be retained. Eventually, dates are checked. + Get a tree-like object of the certificate list """ - untrusted = untrusted or [] - for a in anchors: - chain = Chain(self + untrusted, a) - if len(chain) == 1: # anchor only - continue - # check that the chain does not exclusively rely on untrusted - if any(c in chain[1:] for c in self): - for c in chain: - if c.remainingDays() < 0: - break - if c is chain[-1]: # we got to the end of the chain - return chain - return None - - def verifyChainFromCAFile(self, cafile, untrusted_file=None): + # We store the tree object as a dictionary that contains children. + tree = [(x, []) for x in self.rootCAs] + + # We'll empty this list eventually + certList = list(self) + + # We make a list of certificates we have to search children for, and iterate + # through it until it's empty. + todo = list(tree) + + # Iterate + while todo: + cert, children = todo.pop() + for c in certList: + # Check if this certificate matches the one we're looking at + if c.isIssuer(cert) and c != cert: + item = (c, []) + children.append(item) + certList.remove(c) + todo.append(item) + + return tree + + def getchain(self, cert): """ - Does the same job as .verifyChain() but using the list of anchors - from the cafile. As for .verifyChain(), a list of untrusted - certificates can be passed (as a file, this time). + Return a chain of certificate that points from a ROOT CA to a certificate. """ - try: - with open(cafile, "rb") as f: - ca_certs = f.read() - except Exception: - raise Exception("Could not read from cafile") - anchors = [Cert(c) for c in split_pem(ca_certs)] + def _rec_getchain(chain, curtree): + # See if an element of the current tree signs the cert, if so add it to + # the chain, else recurse. + for c, subtree in curtree: + curchain = chain + [c] + # If 'cert' is issued by c + if cert.isIssuer(c): + # Final node of the chain ! + # (add the final cert if not self signed) + if c != cert: + curchain += [cert] + return curchain + else: + # Not the final node of the chain ! Recurse. + curchain = _rec_getchain(curchain, subtree) + if curchain: + return curchain + return None + + chain = _rec_getchain([], self.tree) + if chain is not None: + return CertTree(chain) + else: + return None + + def verify(self, cert): + """ + Verify that a certificate is properly signed. + """ + # Check that we can find a chain to this certificate + if not self.getchain(cert): + raise ValueError("Certificate verification failed !") + + def show(self, ret: bool = False): + """ + Return the CertTree as a string certificate tree + """ + + def _rec_show(c, children, lvl=0): + s = "" + # Process the current CA + if c: + if not c.isSelfSigned(): + s += "%s [Not Self Signed]\n" % c.subject_str + else: + s += "%s [Self Signed]\n" % c.subject_str + s = lvl * " " + s + lvl += 1 + # Process all sub-CAs at a lower level + for child, subchildren in children: + s += _rec_show(child, subchildren, lvl=lvl) + return s + + showed = _rec_show(None, self.tree) + if ret: + return showed + else: + print(showed) + + def __repr__(self): + return "" % ( + len(self), + len(self.rootCAs), + ) - untrusted = None - if untrusted_file: - try: - with open(untrusted_file, "rb") as f: - untrusted_certs = f.read() - except Exception: - raise Exception("Could not read from untrusted_file") - untrusted = [Cert(c) for c in split_pem(untrusted_certs)] - return self.verifyChain(anchors, untrusted) +####### +# CMS # +####### - def verifyChainFromCAPath(self, capath, untrusted_file=None): +# RFC3852 + + +class CMS_Engine: + """ + A utility class to perform CMS/PKCS7 operations, as specified by RFC3852. + + :param store: a ROOT CA certificate list to trust. + :param crls: a list of CRLs to include. This is currently not checked. + """ + + def __init__( + self, + store: CertList, + crls: List[X509_CRL] = [], + ): + self.store = store + self.crls = crls + + def sign( + self, + message: Union[bytes, Packet], + eContentType: ASN1_OID, + cert: Cert, + key: PrivKey, + h: Optional[str] = None, + ): """ - Does the same job as .verifyChainFromCAFile() but using the list - of anchors in capath directory. The directory should (only) contain - certificates files in PEM format. As for .verifyChainFromCAFile(), - a list of untrusted certificates can be passed as a file - (concatenation of the certificates in PEM format). + Sign a message using CMS. + + :param message: the inner content to sign. + :param eContentType: the OID of the inner content. + :param cert: the certificate whose key to use use for signing. + :param key: the private key to use for signing. + :param h: the hash to use (default: same as the certificate's signature) + + We currently only support X.509 certificates ! """ - try: - anchors = [] - for cafile in os.listdir(capath): - with open(os.path.join(capath, cafile), "rb") as fd: - anchors.append(Cert(fd.read())) - except Exception: - raise Exception("capath provided is not a valid cert path") + # RFC3852 - 5.4. Message Digest Calculation Process + h = h or _get_cert_sig_hashname(cert) + hash = hashes.Hash(_get_hash(h)) + hash.update(bytes(message)) + hashed_message = hash.finalize() + + # 5.5. Signature Generation Process + signerInfo = CMS_SignerInfo( + version=1, + sid=CMS_IssuerAndSerialNumber( + issuer=cert.tbsCertificate.issuer, + serialNumber=cert.tbsCertificate.serialNumber, + ), + digestAlgorithm=X509_AlgorithmIdentifier( + algorithm=ASN1_OID(h), + parameters=ASN1_NULL(0), + ), + signedAttrs=[ + X509_Attribute( + type=ASN1_OID("contentType"), + values=[ + X509_AttributeValue(value=eContentType), + ], + ), + X509_Attribute( + type=ASN1_OID("messageDigest"), + # "A message-digest attribute MUST have a single attribute value" + values=[ + X509_AttributeValue(value=ASN1_STRING(hashed_message)), + ], + ), + ], + signatureAlgorithm=cert.tbsCertificate.signature, + ) + signerInfo.signature = ASN1_STRING( + key.sign( + bytes( + CMS_SignedAttrsForSignature( + signedAttrs=signerInfo.signedAttrs, + ) + ), + h=h, + ) + ) - untrusted = None - if untrusted_file: - try: - with open(untrusted_file, "rb") as f: - untrusted_certs = f.read() - except Exception: - raise Exception("Could not read from untrusted_file") - untrusted = [Cert(c) for c in split_pem(untrusted_certs)] + # Build a chain of X509_Cert to ship (but skip the ROOT certificate) + certTree = CertTree(cert, self.store) + certificates = [x.x509Cert for x in certTree if not x.isSelfSigned()] + + # Build final structure + return CMS_ContentInfo( + contentType=ASN1_OID("id-signedData"), + content=CMS_SignedData( + version=3 if certificates else 1, + digestAlgorithms=X509_AlgorithmIdentifier( + algorithm=ASN1_OID(h), + parameters=ASN1_NULL(0), + ), + encapContentInfo=CMS_EncapsulatedContentInfo( + eContentType=eContentType, + eContent=message, + ), + certificates=( + [CMS_CertificateChoices(certificate=cert) for cert in certificates] + if certificates + else None + ), + crls=( + [CMS_RevocationInfoChoice(crl=crl) for crl in self.crls] + if self.crls + else None + ), + signerInfos=[ + signerInfo, + ], + ), + ) - return self.verifyChain(anchors, untrusted) + def verify( + self, + contentInfo: CMS_ContentInfo, + eContentType: Optional[ASN1_OID] = None, + ): + """ + Verify a CMS message against the list of trusted certificates, + and return the unpacked message if the verification succeeds. - def __repr__(self): - llen = len(self) - 1 - if llen < 0: - return "" - c = self[0] - s = "__ " - if not c.isSelfSigned(): - s += "%s [Not Self Signed]\n" % c.subject_str - else: - s += "%s [Self Signed]\n" % c.subject_str - idx = 1 - while idx <= llen: - c = self[idx] - s += "%s_ %s" % (" " * idx * 2, c.subject_str) - if idx != llen: - s += "\n" - idx += 1 - return s + :param contentInfo: the ContentInfo whose signature to verify + :param eContentType: if provided, verifies that the content type is valid + """ + if contentInfo.contentType.oidname != "id-signedData": + raise ValueError("ContentInfo isn't signed !") + + signeddata = contentInfo.content + + # Build the certificate chain + certificates = [] + if signeddata.certificates: + certificates = [Cert(x.certificate) for x in signeddata.certificates] + certTree = CertTree(certificates, self.store) + + # Check there's at least one signature + if not signeddata.signerInfos: + raise ValueError("ContentInfo contained no signature !") + + # Check all signatures + for signerInfo in signeddata.signerInfos: + # Find certificate in the chain that did this + cert: Cert = certTree.findCertBySid(signerInfo.sid) + + # Verify certificate signature + certTree.verify(cert) + + # Verify the message hash + if signerInfo.signedAttrs: + # Verify the contentType + try: + contentType = next( + x.values[0].value + for x in signerInfo.signedAttrs + if x.type.oidname == "contentType" + ) + + if contentType != signeddata.encapContentInfo.eContentType: + raise ValueError( + "Inconsistent 'contentType' was detected in packet !" + ) + + if eContentType is not None and eContentType != contentType: + raise ValueError( + "Expected '%s' but got '%s' contentType !" + % ( + eContentType, + contentType, + ) + ) + except StopIteration: + raise ValueError("Missing contentType in signedAttrs !") + + # Verify the messageDigest value + try: + # "A message-digest attribute MUST have a single attribute value" + messageDigest = next( + x.values[0].value + for x in signerInfo.signedAttrs + if x.type.oidname == "messageDigest" + ) + + # Re-calculate hash + h = signerInfo.digestAlgorithm.algorithm.oidname + hash = hashes.Hash(_get_hash(h)) + hash.update(bytes(signeddata.encapContentInfo.eContent)) + hashed_message = hash.finalize() + + if hashed_message != messageDigest: + raise ValueError("Invalid messageDigest value !") + except StopIteration: + raise ValueError("Missing messageDigest in signedAttrs !") + + # Verify the signature + cert.verify( + msg=bytes( + CMS_SignedAttrsForSignature( + signedAttrs=signerInfo.signedAttrs, + ) + ), + sig=signerInfo.signature.val, + ) + else: + cert.verify( + msg=bytes(signeddata.encapContentInfo), + sig=signerInfo.signature.val, + ) + + # Return the content + return signeddata.encapContentInfo.eContent diff --git a/scapy/layers/tls/handshake_sslv2.py b/scapy/layers/tls/handshake_sslv2.py index 1917cdf523e..e4a95cabf8e 100644 --- a/scapy/layers/tls/handshake_sslv2.py +++ b/scapy/layers/tls/handshake_sslv2.py @@ -318,7 +318,7 @@ def post_build(self, pkt, pay): else: self.decryptedkey = key - pubkey = self.tls_session.server_certs[0].pubKey + pubkey = self.tls_session.server_certs[0].pubkey self.encryptedkey = pubkey.encrypt(self.decryptedkey) if self.keyarg == b"" and cs_cls.cipher_alg.type == "block": diff --git a/scapy/layers/tls/keyexchange.py b/scapy/layers/tls/keyexchange.py index 59049e1e6d3..20325240c65 100644 --- a/scapy/layers/tls/keyexchange.py +++ b/scapy/layers/tls/keyexchange.py @@ -140,7 +140,7 @@ def getfield(self, pkt, m): s = pkt.tls_session if s.tls_version and s.tls_version < 0x0300: if len(s.client_certs) > 0: - sig_len = s.client_certs[0].pubKey.pubkey.key_size // 8 + sig_len = s.client_certs[0].pubkey.pubkey.key_size // 8 else: warning("No client certificate provided. " "We're making a wild guess about the signature size.") diff --git a/scapy/layers/tpm.py b/scapy/layers/tpm.py new file mode 100644 index 00000000000..d51fabb03b7 --- /dev/null +++ b/scapy/layers/tpm.py @@ -0,0 +1,729 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# Copyright (C) Gabriel Potter + +""" +Implementation of structures related to TPM 2.0 and Windows PCP + +(Windows Plateform Crypto Provider) +""" + +from scapy.config import conf +from scapy.packet import Packet +from scapy.fields import ( + BitField, + ByteField, + ConditionalField, + FlagsField, + IntField, + LEIntEnumField, + LEIntField, + LongField, + MultipleTypeField, + PacketField, + PacketLenField, + PacketListField, + ShortEnumField, + ShortField, + StrField, + StrFixedLenField, + StrLenField, +) + + +########################## +# TPM 2 structures # +########################## + +IMPLEMENTATION_PCR = 24 +PCR_SELECT_MAX = (IMPLEMENTATION_PCR + 7) // 8 +MAX_RSA_KEY_BITS = 2048 +MAX_RSA_KEY_BYTES = (MAX_RSA_KEY_BITS + 7) // 8 + +# TPM20.h source + +TPM_ALG = { + 0x0000: "TPM_ALG_ERROR", + 0x0001: "TPM_ALG_RSA", + 0x0004: "TPM_ALG_SHA1", + 0x0005: "TPM_ALG_HMAC", + 0x0006: "TPM_ALG_AES", + 0x0007: "TPM_ALG_MGF1", + 0x0008: "TPM_ALG_KEYEDHASH", + 0x000A: "TPM_ALG_XOR", + 0x000B: "TPM_ALG_SHA256", + 0x000C: "TPM_ALG_SHA384", + 0x000D: "TPM_ALG_SHA512", + 0x0010: "TPM_ALG_NULL", + 0x0012: "TPM_ALG_SM3_256", + 0x0013: "TPM_ALG_SM4", + 0x0014: "TPM_ALG_RSASSA", + 0x0015: "TPM_ALG_RSAES", + 0x0016: "TPM_ALG_RSAPSS", + 0x0017: "TPM_ALG_OAEP", + 0x0018: "TPM_ALG_ECDSA", + 0x0019: "TPM_ALG_ECDH", + 0x001A: "TPM_ALG_ECDAA", + 0x001B: "TPM_ALG_SM2", + 0x001C: "TPM_ALG_ECSCHNORR", + 0x001D: "TPM_ALG_ECMQV", + 0x0020: "TPM_ALG_KDF1_SP800_56a", + 0x0021: "TPM_ALG_KDF2", + 0x0022: "TPM_ALG_KDF1_SP800_108", + 0x0023: "TPM_ALG_ECC", + 0x0025: "TPM_ALG_SYMCIPHER", + 0x0040: "TPM_ALG_CTR", + 0x0041: "TPM_ALG_OFB", + 0x0042: "TPM_ALG_CBC", + 0x0043: "TPM_ALG_CFB", + 0x0044: "TPM_ALG_ECB", +} + +TPM_ST = { + 0x00C4: "TPM_ST_RSP_COMMAND", + 0x8000: "TPM_ST_NULL", + 0x8001: "TPM_ST_NO_SESSIONS", + 0x8002: "TPM_ST_SESSIONS", + 0x8014: "TPM_ST_ATTEST_NV", + 0x8015: "TPM_ST_ATTEST_COMMAND_AUDIT", + 0x8016: "TPM_ST_ATTEST_SESSION_AUDIT", + 0x8017: "TPM_ST_ATTEST_CERTIFY", + 0x8018: "TPM_ST_ATTEST_QUOTE", + 0x8019: "TPM_ST_ATTEST_TIME", + 0x801A: "TPM_ST_ATTEST_CREATION", + 0x8021: "TPM_ST_CREATION", + 0x8022: "TPM_ST_VERIFIED", + 0x8023: "TPM_ST_AUTH_SECRET", + 0x8024: "TPM_ST_HASHCHECK", + 0x8025: "TPM_ST_AUTH_SIGNED", + 0x8029: "TPM_ST_FU_MANIFEST", +} + + +class _Packet(Packet): + def default_payload_class(self, payload): + return conf.padding_layer + + +class TPMS_SCHEME_SIGHASH(_Packet): + fields_desc = [ + ShortEnumField("hashAlg", 0, TPM_ALG), + ] + + +class TPMT_RSA_SCHEME(_Packet): + fields_desc = [ + ShortEnumField("scheme", 0, TPM_ALG), + # TPMU_ASYM_SCHEME + MultipleTypeField( + [ + ( + PacketField( + "parameters", TPMS_SCHEME_SIGHASH(), TPMS_SCHEME_SIGHASH + ), + lambda pkt: pkt.scheme + in [ + 0x0014, # RSASSA + 0x0016, # RSAPSS + 0x001A, # RSAPSS + 0x001B, # SM2 + 0x001C, # ECSCHNORR + ], + ) + ], + StrFixedLenField("parameters", b"", length=0), + ), + ] + + +class TPMT_SYM_DEF_OBJECT(_Packet): + fields_desc = [ + ShortEnumField("algorithm", 0, TPM_ALG), + ConditionalField( + ShortField("keyBits", 0), + lambda pkt: pkt.algorithm != 0x0010, + ), + ConditionalField( + ShortField("mode", 0), + lambda pkt: pkt.algorithm != 0x0010, + ), + ] + + +class TPMS_RSA_PARMS(_Packet): + fields_desc = [ + PacketField("symmetric", TPMT_SYM_DEF_OBJECT(), TPMT_SYM_DEF_OBJECT), + PacketField("scheme", TPMT_RSA_SCHEME(), TPMT_RSA_SCHEME), + ShortField("keyBits", 0), + IntField("exponent", 0), + ] + + +class TPM2B_DIGEST(_Packet): + fields_desc = [ + ShortField("size", 0), + StrLenField("buffer", b"", length_from=lambda pkt: pkt.size), + ] + + +class TPML_DIGEST(_Packet): + fields_desc = [ + IntField("count", 0), + PacketListField("digests", [], TPM2B_DIGEST, count_from=lambda pkt: pkt.count), + ] + + +class TPMS_NULL_PARMS(_Packet): + fields_desc = [ + ShortEnumField("algorithm", 0x0010, TPM_ALG), + ] + + +class TPMT_PUBLIC(_Packet): + fields_desc = [ + ShortEnumField("type", 0x0001, TPM_ALG), + ShortEnumField("nameAlg", 0, TPM_ALG), + FlagsField( + "objectAttributes", + 0, + 32, + [ + "reserved1", + "fixedTPM", + "stClear", + "reserved4", + "fixedParent", + "sensitiveDataOrigin", + "userWithAuth", + "adminWithPolicy", + "reserved8", + "reserved9", + "noDA", + "encryptedDuplication", + "reserved12", + "reserved13", + "reserved14", + "reserved15", + "restricted", + "decrypt", + "sign", + ], + ), + PacketField("authPolicy", TPM2B_DIGEST(), TPM2B_DIGEST), + MultipleTypeField( + [ + # TPMU_PUBLIC_PARMS + ( + PacketField("parameters", TPMS_RSA_PARMS(), TPMS_RSA_PARMS), + lambda pkt: pkt.type == 0x0001, + ) + ], + StrFixedLenField("parameters", b"", length=0), + ), + # TPMU_PUBLIC_ID + PacketField("unique", TPM2B_DIGEST(), TPM2B_DIGEST), + ] + + +class TPM2B_PUBLIC(_Packet): + fields_desc = [ + ShortField("size", 0), + PacketLenField( + "publicArea", + TPMT_PUBLIC(), + TPMT_PUBLIC, + length_from=lambda pkt: pkt.size, + ), + ] + + +class TPM2B_PRIVATE_KEY_RSA(_Packet): + fields_desc = [ + ShortField("size", 0), + StrLenField( + "buffer", + b"", + length_from=lambda pkt: pkt.size, + ), + ] + + +TPM2B_AUTH = TPM2B_DIGEST + + +class TPMT_SENSITIVE(_Packet): + fields_desc = [ + ShortEnumField("sensitiveType", 0, TPM_ALG), + PacketField("authValue", TPM2B_AUTH(), TPM2B_AUTH), + PacketField("seedValue", TPM2B_DIGEST(), TPM2B_DIGEST), + MultipleTypeField( + [ + # TPMU_SENSITIVE_COMPOSITE + ( + PacketField( + "sensitive", TPM2B_PRIVATE_KEY_RSA(), TPM2B_PRIVATE_KEY_RSA + ), + lambda pkt: pkt.sensitiveType == 0x0001, # TPM_ALG_RSA + ), + ], + StrField("sensitive", b""), + ), + ] + + +class TPM2B_SENSITIVE(_Packet): + fields_desc = [ + ShortField("size", 0), + PacketLenField( + "sensitiveArea", + TPMT_SENSITIVE(), + TPMT_SENSITIVE, + length_from=lambda pkt: pkt.size, + ), + ] + + +class _PRIVATE(_Packet): + fields_desc = [ + PacketField("integrityOuter", TPM2B_DIGEST(), TPM2B_DIGEST), + PacketField("integrityInner", TPM2B_DIGEST(), TPM2B_DIGEST), + StrField("sensitive", b""), # Encrypted + ] + + +class TPM2B_PRIVATE(_Packet): + fields_desc = [ + ShortField("size", 0), + PacketLenField( + "buffer", + _PRIVATE(), + _PRIVATE, + length_from=lambda pkt: pkt.size, + ), + ] + + +class TPM2B_NAME(_Packet): + fields_desc = [ + ShortField("size", 0), + StrLenField("Name", b"", length_from=lambda pkt: pkt.size), + ] + + +class TPM2B_DATA(_Packet): + fields_desc = [ + ShortField("size", 0), + StrLenField("buffer", b"", length_from=lambda pkt: pkt.size), + ] + + +class TPMA_LOCALITY(_Packet): + fields_desc = [ + BitField("locZero", 0, 1), + BitField("locOne", 0, 1), + BitField("locTwo", 0, 1), + BitField("locThree", 0, 1), + BitField("locFour", 0, 1), + BitField("Extended", 0, 3), + ] + + +class TPMS_PCR_SELECTION(_Packet): + fields_desc = [ + ShortEnumField("hash", 0, TPM_ALG), + ByteField("sizeOfSelect", 0), + StrFixedLenField("pcrSelect", b"", length=PCR_SELECT_MAX), + ] + + +class TPML_PCR_SELECTION(_Packet): + fields_desc = [ + IntField("count", 0), + PacketListField( + "pcrSelections", [], TPMS_PCR_SELECTION, count_from=lambda pkt: pkt.count + ), + ] + + +class TPMS_CREATION_DATA(_Packet): + fields_desc = [ + PacketField("pcrSelect", TPML_PCR_SELECTION(), TPML_PCR_SELECTION), + PacketField("pcrDigest", TPM2B_DIGEST(), TPM2B_DIGEST), + PacketField("locality", TPMA_LOCALITY(), TPMA_LOCALITY), + ShortEnumField("parentNameAlg", 0, TPM_ALG), + PacketField("parentName", TPM2B_NAME(), TPM2B_NAME), + PacketField("parentQualifiedName", TPM2B_NAME(), TPM2B_NAME), + PacketField("outsideInfo", TPM2B_DATA(), TPM2B_DATA), + ] + + +class TPM2B_CREATION_DATA(_Packet): + fields_desc = [ + ShortField("size", 0), + PacketLenField( + "creationData", + TPMS_CREATION_DATA(), + TPMS_CREATION_DATA, + length_from=lambda pkt: pkt.size, + ), + ] + + +class TPMS_CLOCK_INFO(_Packet): + fields_desc = [ + LongField("clock", 0), # obfuscated + IntField("resetCount", 0), # obfuscated + IntField("restartCount", 0), # obfuscated + ByteField("safe", 0), + ] + + +class TPMS_CREATION_INFO(_Packet): + fields_desc = [ + PacketField("objectName", TPM2B_NAME(), TPM2B_NAME), + PacketField("creationHash", TPM2B_DIGEST(), TPM2B_DIGEST), + ] + + +class TPMS_CERTIFY_INFO(_Packet): + fields_desc = [ + PacketField("Name", TPM2B_NAME(), TPM2B_NAME), + PacketField("qualifiedName", TPM2B_DIGEST(), TPM2B_DIGEST), + ] + + +class TPMS_ATTEST(_Packet): + fields_desc = [ + StrFixedLenField("magic", b"\xffTCG", length=4), + ShortEnumField("type", 0, TPM_ST), + PacketField("qualifiedSigned", TPM2B_NAME(), TPM2B_NAME), + PacketField("extraData", TPM2B_DATA(), TPM2B_DATA), + PacketField("clockInfo", TPMS_CLOCK_INFO(), TPMS_CLOCK_INFO), + LongField("firmwareVersion", 0), + MultipleTypeField( + [ + # TPMU_ATTEST + ( + PacketField("attested", TPMS_CERTIFY_INFO(), TPMS_CERTIFY_INFO), + lambda pkt: pkt.type == 0x8017, # TPM_ST_ATTEST_CERTIFY + ), + ( + PacketField("attested", TPMS_CREATION_INFO(), TPMS_CREATION_INFO), + lambda pkt: pkt.type == 0x801A, # TPM_ST_ATTEST_CREATION + ), + ], + StrField("attested", b""), + ), + ] + + +class TPM2B_ATTEST(_Packet): + fields_desc = [ + ShortField("size", 0), + PacketLenField( + "attestationData", + TPMS_ATTEST(), + TPMS_ATTEST, + length_from=lambda pkt: pkt.size, + ), + ] + + +class TPM2B_PUBLIC_KEY_RSA(_Packet): + fields_desc = [ + ShortField("size", 0), + StrFixedLenField("buffer", b"", length=MAX_RSA_KEY_BYTES), + ] + + +class TPMS_SIGNATURE_RSASSA(_Packet): + fields_desc = [ + ShortEnumField("hash", 0, TPM_ALG), + PacketField("sig", TPM2B_PUBLIC_KEY_RSA(), TPM2B_PUBLIC_KEY_RSA), + ] + + +class TPMS_SIGNATURE_RSAPSS(_Packet): + fields_desc = [ + ShortEnumField("hash", 0, TPM_ALG), + PacketField("sig", TPM2B_PUBLIC_KEY_RSA(), TPM2B_PUBLIC_KEY_RSA), + ] + + +class TPMT_SIGNATURE(_Packet): + fields_desc = [ + ShortEnumField("sigAlg", 0, TPM_ALG), + MultipleTypeField( + [ + # TPMU_SIGNATURE + ( + PacketField( + "signature", TPMS_SIGNATURE_RSASSA(), TPMS_SIGNATURE_RSASSA + ), + lambda pkt: pkt.sigAlg == 0x0014, # RSASSA + ), + ( + PacketField( + "signature", TPMS_SIGNATURE_RSAPSS(), TPMS_SIGNATURE_RSAPSS + ), + lambda pkt: pkt.sigAlg == 0x0016, # RSASSA + ), + ], + StrField("signature", b""), + ), + ] + + +# From "Using the Windows 8 Platform PCP" documentation +# https://github.com/Microsoft/TSS.MSR/blob/main/PCPTool.v11/inc/TpmAtt.h + + +# NCRYPT_PCP_TPM12_IDBINDING +class PCP_IDBinding20(Packet): + fields_desc = [ + PacketField("PublicKey", TPM2B_PUBLIC(), TPM2B_PUBLIC), + PacketField("CreationData", TPM2B_CREATION_DATA(), TPM2B_CREATION_DATA), + PacketField("Attest", TPM2B_ATTEST(), TPM2B_ATTEST), + PacketField("Signature", TPMT_SIGNATURE(), TPMT_SIGNATURE), + ] + + +_PCP_TYPE = { + 1: "TPM 1.2", + 2: "TPM 2.0", +} + + +class PCP_KEY_BLOB(Packet): + fields_desc = [ + StrFixedLenField("Magic", b"PCPM", length=4), + LEIntField("cbHeader", 0), + LEIntEnumField("pcpType", 1, _PCP_TYPE), + FlagsField( + "flags", + 0, + -32, + { + 0x00000001: "authRequired", + 0x00000002: "undocumented2", + }, + ), + LEIntField("cbTpmKey", 0), + StrLenField( + "tpmKey", + b"", + length_from=lambda pkt: pkt.cbTpmKey, + ), + ] + + +class PCP_20_KEY_BLOB(Packet): + fields_desc = [ + StrFixedLenField("Magic", b"PCPM", length=4), + LEIntField("cbHeader", 0), + LEIntEnumField("pcpType", 2, _PCP_TYPE), + FlagsField( + "flags", + 0, + -32, + { + 0x00000001: "authRequired", + 0x00000002: "undocumented2", + }, + ), + LEIntField("cbPublic", 0), + LEIntField("cbPrivate", 0), + LEIntField("cbMigrationPublic", 0), + LEIntField("cbMigrationPrivate", 0), + LEIntField("cbPolicyDigestList", 0), + LEIntField("cbPCRBinding", 0), + LEIntField("cbPCRDigest", 0), + LEIntField("cbEncryptedSecret", 0), + LEIntField("cbTpm12HostageBlob", 0), + LEIntField("pcrAlgId", 0), + PacketLenField( + "public", + TPM2B_PUBLIC(), + TPM2B_PUBLIC, + length_from=lambda pkt: pkt.cbPublic, + ), + PacketLenField( + "private", + TPM2B_PRIVATE(), + TPM2B_PRIVATE, + length_from=lambda pkt: pkt.cbPrivate, + ), + PacketLenField( + "migrationPublic", + None, + TPM2B_PUBLIC, + length_from=lambda pkt: pkt.cbMigrationPublic, + ), + PacketLenField( + "migrationPrivate", + TPM2B_PRIVATE(), + TPM2B_PRIVATE, + length_from=lambda pkt: pkt.cbMigrationPrivate, + ), + PacketLenField( + "policyDigestList", + TPML_DIGEST(), + TPML_DIGEST, + length_from=lambda pkt: pkt.cbPolicyDigestList, + ), + StrLenField( + "pcrBinding", + b"", + length_from=lambda pkt: pkt.cbPCRBinding, + ), + StrLenField( + "pcrDigest", + b"", + length_from=lambda pkt: pkt.cbPCRDigest, + ), + StrLenField( + "encryptedSecret", + b"", + length_from=lambda pkt: pkt.cbEncryptedSecret, + ), + StrLenField( + "tpm12HostageBlob", + b"", + length_from=lambda pkt: pkt.cbTpm12HostageBlob, + ), + ] + + +########################### +# Microsoft Windows # +########################### + +# [MS-WCCE] sect 2.2.2.5 + + +class KeyAttestation(Packet): + fields_desc = [ + StrFixedLenField("Magic", b"KADS", length=4), + LEIntEnumField("Platform", 2, _PCP_TYPE), + LEIntField("HeaderSize", 0), + LEIntField("cbKeyAttest", 0), + LEIntField("cbSignature", 0), + LEIntField("cbKeyBlob", 0), + MultipleTypeField( + [ + ( + PacketLenField( + "keyAttest", + TPMS_ATTEST(), + TPMS_ATTEST, + length_from=lambda pkt: pkt.cbKeyAttest, + ), + lambda pkt: pkt.Platform == 2, + ) + ], + StrLenField( + "keyAttest", + b"", + length_from=lambda pkt: pkt.cbKeyAttest, + ), + ), + StrLenField( + "signature", + b"", + length_from=lambda pkt: pkt.cbSignature, + ), + MultipleTypeField( + [ + ( + PacketLenField( + "keyBlob", + PCP_20_KEY_BLOB(), + PCP_20_KEY_BLOB, + length_from=lambda pkt: pkt.cbKeyBlob, + ), + lambda pkt: pkt.Platform == 2, + ), + ( + PacketLenField( + "keyBlob", + PCP_KEY_BLOB(), + PCP_KEY_BLOB, + length_from=lambda pkt: pkt.cbKeyBlob, + ), + lambda pkt: pkt.Platform == 1, + ), + ], + StrLenField( + "keyBlob", + b"", + length_from=lambda pkt: pkt.cbKeyBlob, + ), + ), + ] + + def default_payload_class(self, payload): + return conf.padding_layer + + +class KeyAttestationStatement(Packet): + fields_desc = [ + StrFixedLenField("Magic", b"KAST", length=4), + LEIntField("Version", 1), + LEIntEnumField("Platform", 2, _PCP_TYPE), + LEIntField("HeaderSize", 0), + LEIntField("cbIdBinding", 0), + LEIntField("cbKeyAttestation", 0), + LEIntField("cbAIKOpaque", 0), + MultipleTypeField( + [ + ( + PacketLenField( + "idBinding", + PCP_IDBinding20(), + PCP_IDBinding20, + length_from=lambda pkt: pkt.cbIdBinding, + ), + lambda pkt: pkt.Platform == 2, + ) + ], + StrLenField( + "idBinding", + b"", + length_from=lambda pkt: pkt.cbIdBinding, + ), + ), + PacketLenField( + "keyAttestation", + KeyAttestation(), + KeyAttestation, + length_from=lambda pkt: pkt.cbKeyAttestation, + ), + MultipleTypeField( + [ + ( + PacketLenField( + "aikOpaque", + PCP_20_KEY_BLOB(), + PCP_20_KEY_BLOB, + length_from=lambda pkt: pkt.cbAIKOpaque, + ), + lambda pkt: pkt.Platform == 2, + ), + ( + PacketLenField( + "aikOpaque", + PCP_KEY_BLOB(), + PCP_KEY_BLOB, + length_from=lambda pkt: pkt.cbAIKOpaque, + ), + lambda pkt: pkt.Platform == 1, + ), + ], + StrLenField( + "aikOpaque", + b"", + length_from=lambda pkt: pkt.cbAIKOpaque, + ), + ), + ] diff --git a/scapy/layers/x509.py b/scapy/layers/x509.py index 1f1afe4f9c9..6611e9bc590 100644 --- a/scapy/layers/x509.py +++ b/scapy/layers/x509.py @@ -7,14 +7,14 @@ # Cool history about this file: http://natisbad.org/scapy/index.html """ -X.509 certificates and other crypto-related ASN.1 structures +X.509 certificates, OCSP, CRL, CMS and other crypto-related ASN.1 structures """ +from scapy.asn1.ber import BER_Decoding_Error from scapy.asn1.mib import conf # loads conf.mib from scapy.asn1.asn1 import ( ASN1_Codecs, ASN1_IA5_STRING, - ASN1_NULL, ASN1_OID, ASN1_PRINTABLE_STRING, ASN1_UTC_TIME, @@ -37,12 +37,14 @@ ASN1F_ISO646_STRING, ASN1F_NULL, ASN1F_OID, + ASN1F_omit, ASN1F_optional, ASN1F_PACKET, ASN1F_PRINTABLE_STRING, ASN1F_SEQUENCE_OF, ASN1F_SEQUENCE, ASN1F_SET_OF, + ASN1F_STRING_ENCAPS, ASN1F_STRING_PacketField, ASN1F_STRING, ASN1F_T61_STRING, @@ -51,10 +53,15 @@ ASN1F_UTF8_STRING, ) from scapy.packet import Packet -from scapy.fields import PacketField, MultipleTypeField +from scapy.fields import ( + MultipleTypeField, + PacketField, +) from scapy.volatile import ZuluTime, GeneralizedTime from scapy.compat import plain_str +from scapy.layers.tpm import KeyAttestationStatement + class ASN1P_OID(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER @@ -218,6 +225,24 @@ class ECDSASignature(ASN1_Packet): ASN1F_INTEGER("s", 0)) +#################################### +# Diffie Hellman Exchange Packets # +#################################### +# based on PKCS#3 + +# PKCS#3 sect 9 + +class DHParameter(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("p", 0), + ASN1F_INTEGER("g", 0), + ASN1F_optional( + ASN1F_INTEGER("l", 0) # aka. 'privateValueLength' + ), + ) + + #################################### # x25519/x448 packets # #################################### @@ -266,12 +291,37 @@ def __init__(self, name, default, **kwargs): **kwargs) +# More details on attributes in PKCS#9 +_X509_ATTRIBUTE_TYPE = {} + + +class _AttributeValue_Field(ASN1F_field): + def m2i(self, pkt, s): + # Some types have special structures + if pkt.underlayer: + attrType = pkt.underlayer.type.val + if attrType in _X509_ATTRIBUTE_TYPE: + return self.extract_packet( + _X509_ATTRIBUTE_TYPE[attrType], + s, + _underlayer=pkt, + ) + try: + return super(_AttributeValue_Field, self).m2i(pkt, s) + except BER_Decoding_Error: + # Do not fail on special attributes + return s, b"" + + def i2m(self, pkt, x): + # The special structures should be just bytes() + if pkt.underlayer and pkt.underlayer.type.val in _X509_ATTRIBUTE_TYPE: + return bytes(x) + return super(_AttributeValue_Field, self).i2m(pkt, x) + + class X509_AttributeValue(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_CHOICE("value", ASN1_PRINTABLE_STRING("FR"), - ASN1F_PRINTABLE_STRING, ASN1F_UTF8_STRING, - ASN1F_IA5_STRING, ASN1F_T61_STRING, - ASN1F_UNIVERSAL_STRING) + ASN1_root = _AttributeValue_Field("value", ASN1_PRINTABLE_STRING("FR")) class X509_Attribute(ASN1_Packet): @@ -771,6 +821,21 @@ class X509_ExtOidNTDSCaSecurity(ASN1_Packet): value = ASN1_UTF8_STRING("") +# [MS-WCCE] sect 2.2.2.7.7.2 + +class X509_ExtCertificateTemplateOID(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("templateID", "0"), + ASN1F_optional( + ASN1F_INTEGER("templateMajorVersion", 0), + ), + ASN1F_optional( + ASN1F_INTEGER("templateMinorVersion", 0), + ), + ) + + # oid-info.com shows that some extensions share multiple OIDs. # Here we only reproduce those written in RFC5280. _ext_mapping = { @@ -799,6 +864,8 @@ class X509_ExtOidNTDSCaSecurity(ASN1_Packet): "2.16.840.1.113730.1.1": X509_ExtNetscapeCertType, "2.16.840.1.113730.1.13": X509_ExtComment, "1.3.6.1.4.1.311.20.2": X509_ExtCertificateTemplateName, + "1.3.6.1.4.1.311.21.7": X509_ExtCertificateTemplateOID, + "1.3.6.1.4.1.311.21.10": X509_ExtCertificatePolicies, "1.3.6.1.4.1.311.25.2": X509_ExtOidNTDSCaSecurity, "1.3.6.1.5.5.7.1.1": X509_ExtAuthInfoAccess, "1.3.6.1.5.5.7.1.3": X509_ExtQcStatements, @@ -841,19 +908,84 @@ class X509_Extensions(ASN1_Packet): None, X509_Extension)) +# Aka 'ExtensionReq' in CMS +_X509_ATTRIBUTE_TYPE["1.2.840.113549.1.9.14"] = X509_Extensions + + # Public key wrapper # class X509_AlgorithmIdentifier(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_OID("algorithm", "1.2.840.113549.1.1.11"), - ASN1F_optional( - ASN1F_CHOICE( - "parameters", ASN1_NULL(0), - ASN1F_NULL, - ECParameters, - DomainParameters, - ) + MultipleTypeField( + [ + ( + # RFC4055: + # "The correct encoding is to omit the parameters field" + # "All implementations MUST accept both NULL and absent + # parameters as legal and equivalent encodings." + + # RFC8017: + # "should generally be omitted, but if present, it shall have a + # value of type NULL." + ASN1F_optional(ASN1F_NULL("parameters", None)), + lambda pkt: ( + pkt.algorithm.val[:19] == "1.2.840.113549.1.1." or + pkt.algorithm.val[:21] == "2.16.840.1.101.3.4.2." or + pkt.algorithm.val[:11] == "1.3.14.3.2." + ) + ), + ( + # RFC5758: + # "the encoding MUST omit the parameters field" + + # RFC8410: + # "For all of the OIDs, the parameters MUST be absent." + ASN1F_omit("parameters", None), + lambda pkt: ( + pkt.algorithm.val[:16] == "1.2.840.10045.4." or + pkt.algorithm.val in ["1.3.101.112", "1.3.101.113"] + ) + ), + # RFC5480 + ( + ASN1F_PACKET( + "parameters", + ECParameters(), + ECParameters, + ), + lambda pkt: pkt.algorithm.val == "1.2.840.10045.2.1", + ), + # RFC3279 + ( + ASN1F_PACKET( + "parameters", + DomainParameters(), + DomainParameters, + ), + lambda pkt: pkt.algorithm.val == "1.2.840.10046.2.1", + ), + # PKCS#3 + ( + ASN1F_PACKET( + "parameters", + DHParameter(), + DHParameter, + ), + lambda pkt: pkt.algorithm.val == "1.2.840.113549.1.3.1", + ), + # TripleDES + ( + ASN1F_STRING( + "parameters", + "", + ), + lambda pkt: pkt.algorithm.val == "1.2.840.113549.3.7", + ), + ], + # Default: fail, probably. This is most likely unimplemented. + ASN1F_NULL("parameters", 0), ) ) @@ -969,6 +1101,33 @@ class ECDSAPrivateKey_OpenSSL(Packet): ] +class _IssuerUtils: + def get_issuer(self): + attrs = self.issuer + attrsDict = {} + for attr in attrs: + # we assume there is only one name in each rdn ASN1_SET + attrsDict[attr.rdn[0].type.oidname] = plain_str(attr.rdn[0].value.val) # noqa: E501 + return attrsDict + + def get_issuer_str(self): + """ + Returns a one-line string containing every type/value + in a rather specific order. sorted() built-in ensures unicity. + """ + name_str = "" + attrsDict = self.get_issuer() + for attrType, attrSymbol in _attrName_mapping: + if attrType in attrsDict: + name_str += "/" + attrSymbol + "=" + name_str += attrsDict[attrType] + for attrType in sorted(attrsDict): + if attrType not in _attrName_specials: + name_str += "/" + attrType + "=" + name_str += attrsDict[attrType] + return name_str + + class X509_Validity(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( @@ -991,7 +1150,7 @@ class X509_Validity(ASN1_Packet): _attrName_specials = [name for name, symbol in _attrName_mapping] -class X509_TBSCertificate(ASN1_Packet): +class X509_TBSCertificate(ASN1_Packet, _IssuerUtils): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_optional( @@ -1021,31 +1180,6 @@ class X509_TBSCertificate(ASN1_Packet): X509_Extension, explicit_tag=0xa3))) - def get_issuer(self): - attrs = self.issuer - attrsDict = {} - for attr in attrs: - # we assume there is only one name in each rdn ASN1_SET - attrsDict[attr.rdn[0].type.oidname] = plain_str(attr.rdn[0].value.val) # noqa: E501 - return attrsDict - - def get_issuer_str(self): - """ - Returns a one-line string containing every type/value - in a rather specific order. sorted() built-in ensures unicity. - """ - name_str = "" - attrsDict = self.get_issuer() - for attrType, attrSymbol in _attrName_mapping: - if attrType in attrsDict: - name_str += "/" + attrSymbol + "=" - name_str += attrsDict[attrType] - for attrType in sorted(attrsDict): - if attrType not in _attrName_specials: - name_str += "/" + attrType + "=" - name_str += attrsDict[attrType] - return name_str - def get_subject(self): attrs = self.subject attrsDict = {} @@ -1105,7 +1239,7 @@ class X509_RevokedCertificate(ASN1_Packet): None, X509_Extension))) -class X509_TBSCertList(ASN1_Packet): +class X509_TBSCertList(ASN1_Packet, _IssuerUtils): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_optional( @@ -1125,31 +1259,6 @@ class X509_TBSCertList(ASN1_Packet): X509_Extension, explicit_tag=0xa0))) - def get_issuer(self): - attrs = self.issuer - attrsDict = {} - for attr in attrs: - # we assume there is only one name in each rdn ASN1_SET - attrsDict[attr.rdn[0].type.oidname] = plain_str(attr.rdn[0].value.val) # noqa: E501 - return attrsDict - - def get_issuer_str(self): - """ - Returns a one-line string containing every type/value - in a rather specific order. sorted() built-in ensures unicity. - """ - name_str = "" - attrsDict = self.get_issuer() - for attrType, attrSymbol in _attrName_mapping: - if attrType in attrsDict: - name_str += "/" + attrSymbol + "=" - name_str += attrsDict[attrType] - for attrType in sorted(attrsDict): - if attrType not in _attrName_specials: - name_str += "/" + attrType + "=" - name_str += attrsDict[attrType] - return name_str - class ASN1F_X509_CRL(ASN1F_SEQUENCE): def __init__(self, **kargs): @@ -1213,7 +1322,7 @@ class CMS_EncapsulatedContentInfo(ASN1_Packet): ASN1F_optional( _EncapsulatedContent_Field("eContent", None, explicit_tag=0xA0), - ) + ), ) @@ -1241,37 +1350,52 @@ class CMS_CertificateChoices(ASN1_Packet): # RFC3852 sect 10.2.4 -class CMS_IssuerAndSerialNumber(ASN1_Packet): +class CMS_IssuerAndSerialNumber(ASN1_Packet, _IssuerUtils): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( - ASN1F_PACKET("issuer", X509_DirectoryName(), X509_DirectoryName), + ASN1F_SEQUENCE_OF("issuer", _default_issuer, X509_RDN), ASN1F_INTEGER("serialNumber", 0) ) -# RFC3852 sect 5.3 - +# RFC3852 sect 10.2.7 -class CMS_Attribute(ASN1_Packet): +class CMS_OtherKeyAttribute(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( - ASN1F_OID("attrType", "0"), - ASN1F_SET_OF("attrValues", [], ASN1F_field("attr", None)) + ASN1F_OID("keyAttrId", "0"), + ASN1F_field("keyAttr", 0), ) +# RFC3852 sect 5.3 + + +class CMS_SubjectKeyIdentifier(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_STRING("sid", "") + + class CMS_SignerInfo(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( CMSVersion("version", 1), - ASN1F_PACKET("sid", CMS_IssuerAndSerialNumber(), CMS_IssuerAndSerialNumber), + ASN1F_CHOICE( + "sid", + CMS_IssuerAndSerialNumber(), + ASN1F_PACKET("sid", CMS_IssuerAndSerialNumber(), + CMS_IssuerAndSerialNumber), + ASN1F_PACKET("sid", CMS_SubjectKeyIdentifier(), + CMS_SubjectKeyIdentifier, + implicit_tag=0x80), + ), ASN1F_PACKET("digestAlgorithm", X509_AlgorithmIdentifier(), X509_AlgorithmIdentifier), ASN1F_optional( ASN1F_SET_OF( "signedAttrs", None, - CMS_Attribute, + X509_Attribute, implicit_tag=0xA0, ) ), @@ -1282,13 +1406,24 @@ class CMS_SignerInfo(ASN1_Packet): ASN1F_SET_OF( "unsignedAttrs", None, - CMS_Attribute, + X509_Attribute, implicit_tag=0xA1, ) ) ) +# RFC3852 sect 5.4 + +class CMS_SignedAttrsForSignature(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF( + "signedAttrs", + None, + X509_Attribute, + ) + + # RFC3852 sect 5.1 class CMS_SignedData(ASN1_Packet): @@ -1321,9 +1456,160 @@ class CMS_SignedData(ASN1_Packet): ), ) -# RFC3852 sect 3 + +# RFC3852 sect 6.2.1 + +class CMS_KeyTransRecipientInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + CMSVersion("version", 0), + ASN1F_CHOICE( + "rid", + CMS_IssuerAndSerialNumber(), + ASN1F_PACKET("rid", CMS_IssuerAndSerialNumber(), + CMS_IssuerAndSerialNumber), + ASN1F_PACKET("rid", CMS_SubjectKeyIdentifier(), + CMS_SubjectKeyIdentifier, + implicit_tag=0x80), + ), + ASN1F_PACKET("keyEncryptionAlgorithm", + X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_STRING("encryptedKey", ""), + ) + + +# RFC3852 sect 6.2.2 + +class CMS_OriginatorPublicKey(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("algorithm", + X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_BIT_STRING("publicKey", ""), + ) +class CMS_OriginatorIdentifierOrKey(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "originator", + CMS_IssuerAndSerialNumber(), + ASN1F_PACKET("issuerAndSerialNumber", CMS_IssuerAndSerialNumber(), + CMS_IssuerAndSerialNumber), + ASN1F_PACKET("subjectKeyIdentifier", CMS_SubjectKeyIdentifier(), + CMS_SubjectKeyIdentifier, + implicit_tag=0x80), + ASN1F_PACKET("originatorKey", CMS_OriginatorPublicKey(), + CMS_OriginatorPublicKey, + implicit_tag=0xA1), + ) + + +class CMS_RecipientEncryptedKey(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("subjectKeyIdentifier", CMS_SubjectKeyIdentifier(), + CMS_SubjectKeyIdentifier), + ASN1F_optional( + ASN1F_GENERALIZED_TIME("date", ""), + ), + ASN1F_optional( + ASN1F_PACKET("other", CMS_OtherKeyAttribute(), CMS_OtherKeyAttribute), + ), + ) + + +class CMS_KeyAgreeRecipientInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + CMSVersion("version", 3), + ASN1F_PACKET("originator", CMS_OriginatorIdentifierOrKey(), + CMS_OriginatorIdentifierOrKey, + explicit_tag=0xA0), + ASN1F_optional( + ASN1F_STRING("ukm", None, "", + explicit_tag=0x81), + ), + ASN1F_PACKET("keyEncryptionAlgorithm", + X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_SEQUENCE_OF("recipientEncryptedKeys", [], CMS_RecipientEncryptedKey), + ) + + +# RFC3852 sect 6.2 + +class CMS_RecipientInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "recipientInfo", + CMS_KeyTransRecipientInfo(), + ASN1F_PACKET("ktri", CMS_KeyTransRecipientInfo(), CMS_KeyTransRecipientInfo), + ASN1F_PACKET("kari", CMS_KeyAgreeRecipientInfo(), CMS_KeyAgreeRecipientInfo, + implicit_tag=0xA1), + ) + + +# RFC3852 sect 6.1 + +class CMS_OriginatorInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional( + ASN1F_SET_OF( + "certs", + None, + CMS_CertificateChoices, + implicit_tag=0xA0, + ) + ), + ASN1F_optional( + ASN1F_SET_OF( + "crls", + None, + CMS_RevocationInfoChoice, + implicit_tag=0xA1, + ) + ), + ) + + +class CMS_EncryptedContentInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("contentType", "1.2.840.113549.1.7.2"), + ASN1F_PACKET("contentEncryptionAlgorithm", + X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_optional( + ASN1F_STRING("encryptedContent", "", + implicit_tag=0x80), + ) + ) + + +class CMS_EnvelopedData(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + CMSVersion("version", 1), + ASN1F_optional( + ASN1F_PACKET("originatorInfo", None, CMS_OriginatorInfo, + implicit_tag=0xA0), + ), + ASN1F_SET_OF("recipientInfos", CMS_RecipientInfo(), CMS_RecipientInfo), + ASN1F_PACKET("encryptedContentInfo", CMS_EncryptedContentInfo(), + CMS_EncryptedContentInfo), + ASN1F_optional( + ASN1F_SET_OF("unprotectedAttrs", [], X509_Attribute, + implicit_tag=0xA1), + ) + ) + + +# RFC3852 sect 3 + class CMS_ContentInfo(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( @@ -1334,13 +1620,214 @@ class CMS_ContentInfo(ASN1_Packet): ASN1F_PACKET("content", None, CMS_SignedData, explicit_tag=0xA0), lambda pkt: pkt.contentType.oidname == "id-signedData" - ) + ), + ( + ASN1F_PACKET("content", None, CMS_EnvelopedData, + explicit_tag=0xA0), + lambda pkt: pkt.contentType.oidname == "id-envelopedData" + ), ], ASN1F_BIT_STRING("content", "", explicit_tag=0xA0) ) ) +##################### +# CSR packets # +##################### + +# based on PKCS#10 # + + +class PKCS10_CertificationRequestInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("version", 0), + ASN1F_SEQUENCE_OF("subject", _default_subject, X509_RDN), + ASN1F_PACKET("subjectPublicKeyInfo", + X509_SubjectPublicKeyInfo(), + X509_SubjectPublicKeyInfo), + ASN1F_SET_OF("attributes", [], X509_Attribute, + implicit_tag=0xA0), + ) + + get_subject = X509_TBSCertificate.get_subject + get_subject_str = X509_TBSCertificate.get_subject_str + + +class PKCS10_CertificationRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("certificationRequestInfo", PKCS10_CertificationRequestInfo(), + PKCS10_CertificationRequestInfo), + ASN1F_PACKET("signatureAlgorithm", X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_BIT_STRING("signature", ASN1F_BIT_STRING("", "")), + ) + + +# based on CMC # + +# RFC 5272 sect 3.2.1.1 + +class CMC_TaggedAttribute(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("bodyPartID", 0), + ASN1F_OID("type", "0"), # attrType for compat + ASN1F_SET_OF("attrValues", [], X509_AttributeValue), + ) + + +# RFC 5272 sect 3.2.1.2.1 + +class CMC_TaggedCertificationRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("bodyPartID", 0), + ASN1F_PACKET("certificationRequest", PKCS10_CertificationRequest(), + PKCS10_CertificationRequest) + ) + + +# RFC 5272 sect 3.2.1.2 + +class CMC_TaggedRequest(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "request", CMC_TaggedCertificationRequest(), + ASN1F_PACKET("tcr", CMC_TaggedCertificationRequest(), + CMC_TaggedCertificationRequest, + implicit_tag=0xA0), + # XXX there are others + ) + + +# RFC 5272 sect 3.2.1.3 + +class CMC_TaggedContentInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("bodyPartID", 0), + ASN1F_PACKET("contentInfo", CMS_ContentInfo(), + CMS_ContentInfo) + ) + + +# RFC 5272 sect 3.2.1.4 + +class CMC_OtherMsg(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("bodyPartID", 0), + ASN1F_OID("otherMsgType", "0"), + ASN1F_field("otherMsgValue", ""), + ) + + +# RFC 5272 sect 3.2.1 + +class CMC_PKIData(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF("controlSequence", [], CMC_TaggedAttribute), + ASN1F_SEQUENCE_OF("reqSequence", [], CMC_TaggedRequest), + ASN1F_SEQUENCE_OF("cmsSequence", [], CMC_TaggedContentInfo), + ASN1F_SEQUENCE_OF("otherMsgSequence", [], CMC_OtherMsg), + ) + + +_CMS_ENCAPSULATED["1.3.6.1.5.5.7.12.2"] = CMC_PKIData + + +# Windows extensions # + +# https://learn.microsoft.com/en-us/windows/win32/seccertenroll/cmc-extensions + +class CMC_AddExtensions(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("pkiDataReference", 0), + ASN1F_SEQUENCE_OF("certReferences", [], ASN1F_INTEGER), + ASN1F_PACKET("extensions", X509_Extensions(), X509_Extensions), + ) + + +_X509_ATTRIBUTE_TYPE["1.3.6.1.5.5.7.7.8"] = CMC_AddExtensions + + +# https://learn.microsoft.com/en-us/windows/win32/seccertenroll/cmc-attributes + +class CMC_AddAttributes(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("pkiDataReference", 0), + ASN1F_SEQUENCE_OF("certReferences", [], ASN1F_INTEGER), + ASN1F_SET_OF("attributes", X509_Attribute(), X509_Attribute), + ) + + +_X509_ATTRIBUTE_TYPE["1.3.6.1.4.1.311.10.10.1"] = CMC_AddAttributes + + +# [MS-WCCE] sect 2.2.2.7.2 + +class CMC_ENROLLMENT_CSP_PROVIDER(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("KeySpec", 0), + ASN1F_BMP_STRING("ProviderName", ""), + ASN1F_BIT_STRING("Signature", ""), + ) + + +_X509_ATTRIBUTE_TYPE["1.3.6.1.4.1.311.13.2.2"] = CMC_ENROLLMENT_CSP_PROVIDER + + +# [MS-WCCE] sect 2.2.2.7.4 + +class CMC_REQUEST_CLIENT_INFO(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("clientId", 0), + ASN1F_UTF8_STRING("MachineName", ""), + ASN1F_UTF8_STRING("UserName", ""), + ASN1F_UTF8_STRING("ProcessName", ""), + ) + + +_X509_ATTRIBUTE_TYPE["1.3.6.1.4.1.311.21.20"] = CMC_REQUEST_CLIENT_INFO + + +# [MS-WCCE] sect 2.2.2.7.10 + +class CMC_EnrollmentNameValuePair(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BMP_STRING("Name", ""), + ASN1F_BMP_STRING("Value", ""), + ) + + +_X509_ATTRIBUTE_TYPE["1.3.6.1.4.1.311.13.2.1"] = CMC_EnrollmentNameValuePair + + +# [MS-WCCE] sect 2.2.2.7.12 + +class CMC_ENROLL_ATTESTATION_STATEMENT(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_STRING_ENCAPS("kas", KeyAttestationStatement(), + KeyAttestationStatement) + + +_X509_ATTRIBUTE_TYPE["1.3.6.1.4.1.311.21.24"] = CMC_ENROLL_ATTESTATION_STATEMENT + + +# [MS-WCCE] sect 2.2.2.7.13 + +_X509_ATTRIBUTE_TYPE["1.3.6.1.4.1.311.21.23"] = CMS_ContentInfo + + ############################# # OCSP Status packets # ############################# diff --git a/scapy/libs/rfc3961.py b/scapy/libs/rfc3961.py index ed6581ceaff..e07d00c1df9 100644 --- a/scapy/libs/rfc3961.py +++ b/scapy/libs/rfc3961.py @@ -1445,3 +1445,35 @@ def prfplus(key, pepper): ) ), ) + + +############ +# RFC 4556 # +############ + +def octetstring2key(etype: EncryptionType, x: bytes) -> Key: + """ + RFC4556 octetstring2key:: + + octetstring2key(x) == random-to-key(K-truncate( + SHA1(0x00 | x) | + SHA1(0x01 | x) | + SHA1(0x02 | x) | + ... + )) + """ + try: + ep = _enctypes[etype] + except ValueError: + raise ValueError("Unknown etype '%s'" % etype) + + out = b"" + count = 0 + while len(out) < ep.keysize: + out += Hash_SHA().digest(struct.pack("!B", count) + x) + count += 1 + + return Key.random_to_key( + etype=etype, + seed=out[:ep.keysize], + ) diff --git a/scapy/modules/ticketer.py b/scapy/modules/ticketer.py index 87c591753bc..8f8e9bfdd6c 100644 --- a/scapy/modules/ticketer.py +++ b/scapy/modules/ticketer.py @@ -850,7 +850,7 @@ def get_cred(self, principal, etype=None): "Note principals are case sensitive, as on ktpass.exe" ) - def ssp(self, i): + def ssp(self, i, **kwargs): """ Create a KerberosSSP from a ticket or from the keystore. @@ -859,18 +859,31 @@ def ssp(self, i): """ if isinstance(i, int): ticket, sessionkey, upn, spn = self.export_krb(i) - return KerberosSSP( - ST=ticket, - KEY=sessionkey, - UPN=upn, - SPN=spn, - ) + if spn.startswith("krbtgt/"): + # It's a TGT + kwargs.setdefault("SPN", None) # Use target_name only + return KerberosSSP( + TGT=ticket, + KEY=sessionkey, + UPN=upn, + **kwargs, + ) + else: + # It's a ST + return KerberosSSP( + ST=ticket, + KEY=sessionkey, + UPN=upn, + SPN=spn, + **kwargs, + ) elif isinstance(i, str): spn = i key = self.get_cred(spn) return KerberosSSP( SPN=spn, KEY=key, + **kwargs, ) else: raise ValueError("Invalid 'i' value. Must be int or str") @@ -2424,6 +2437,9 @@ def request_tgt( fast=False, armor_with=None, spn=None, + x509=None, + x509key=None, + p12=None, **kwargs, ): """ @@ -2458,6 +2474,9 @@ def request_tgt( armor_ticket_upn=armor_ticket_upn, armor_ticket_skey=armor_ticket_skey, spn=spn, + x509=x509, + x509key=x509key, + p12=p12, **kwargs, ) if not res: @@ -2570,3 +2589,10 @@ def renew(self, i, ip=None, additional_tickets=[], **kwargs): return self.import_krb(res, _inplace=i) + + def iter_tickets(self): + """ + Iterate through the tickets in the ccache + """ + for i in range(len(self.ccache.credentials)): + yield self.export_krb(i) diff --git a/test/configs/cryptography.utsc b/test/configs/cryptography.utsc index 53b307d2897..4963fc71438 100644 --- a/test/configs/cryptography.utsc +++ b/test/configs/cryptography.utsc @@ -4,6 +4,7 @@ "test/scapy/layers/dot11.uts", "test/scapy/layers/ipsec.uts", "test/scapy/layers/kerberos.uts", + "test/scapy/layers/ntlm.uts", "test/scapy/layers/msnrpc.uts", "test/scapy/layers/tls/cert.uts", "test/scapy/layers/tls/tls*.uts" @@ -15,7 +16,6 @@ "test/scapy/layers/tls/*.uts": "load_layer(\"tls\")" }, "kw_ko": [ - "mock", "needs_root" ] } diff --git a/test/contrib/ikev2.uts b/test/contrib/ikev2.uts index 9d51493daa1..21ef35fdfda 100644 --- a/test/contrib/ikev2.uts +++ b/test/contrib/ikev2.uts @@ -781,7 +781,7 @@ frames = [ subjectPublicKeyInfo=X509_SubjectPublicKeyInfo( signatureAlgorithm=X509_AlgorithmIdentifier( algorithm=ASN1_OID('ecPublicKey'), - parameters=ASN1_OID('prime256v1')), + parameters=ECParameters(curve=ASN1_OID('prime256v1'))), subjectPublicKey=ECDSAPublicKey( ecPoint=ASN1_BIT_STRING( '000001001011011101000101011100101010000110110101110111010001110' @@ -1125,7 +1125,7 @@ frames = [ subjectPublicKeyInfo=X509_SubjectPublicKeyInfo( signatureAlgorithm=X509_AlgorithmIdentifier( algorithm=ASN1_OID('ecPublicKey'), - parameters=ASN1_OID('prime256v1') + parameters=ECParameters(curve=ASN1_OID('prime256v1')), ), subjectPublicKey=ECDSAPublicKey( ecPoint=ASN1_BIT_STRING( diff --git a/test/contrib/send.uts b/test/contrib/send.uts index 80d892c0f02..c4ab0ee09cd 100644 --- a/test/contrib/send.uts +++ b/test/contrib/send.uts @@ -10,7 +10,7 @@ assert pkt[ICMPv6NDOptRsaSig].signature_pad == b"\x01" * 12 = ICMPv6NDOptCGA build and dissection -pkt = Ether()/IPv6()/ICMPv6ND_NS()/ICMPv6NDOptCGA(CGA_PARAMS=CGA_Params()) +pkt = Ether()/IPv6()/ICMPv6ND_NS()/ICMPv6NDOptCGA(CGA_PARAMS=CGA_Params(pubkey=X509_SubjectPublicKeyInfo(signatureAlgorithm=X509_AlgorithmIdentifier(parameters=0)))) pkt = Ether(raw(pkt)) assert ICMPv6NDOptCGA in pkt diff --git a/test/scapy/layers/dcerpc.uts b/test/scapy/layers/dcerpc.uts index e496b68719c..27e4d4a9f5c 100644 --- a/test/scapy/layers/dcerpc.uts +++ b/test/scapy/layers/dcerpc.uts @@ -878,6 +878,7 @@ rpcserver = MyRPCServer.spawn( iface=conf.loopback_name, port=12345, bg=True, + debug=4, ) = Functional: Connect to it with DCERPC_Client over NCACN_NP @@ -886,7 +887,7 @@ client = DCERPC_Client( DCERPC_Transport.NCACN_NP, ndr64=False, ) -client.connect(get_if_addr(conf.loopback_name), port=12345) +client.connect(get_if_addr(conf.loopback_name), port=12345, smb_kwargs={"debug": 4}) client.open_smbpipe("wkssvc") client.bind(find_dcerpc_interface("wkssvc")) @@ -931,6 +932,7 @@ rpcserver = MyRPCServer.spawn( ssp=ssp, port=12345, bg=True, + debug=4, ) = Functional: Connect to it with DCERPC_Client over NCACN_NP with NTLMSSP @@ -940,7 +942,7 @@ client = DCERPC_Client( ssp=ssp, ndr64=False, ) -client.connect(get_if_addr(conf.loopback_name), port=12345, smb_kwargs={"debug": 5}) +client.connect(get_if_addr(conf.loopback_name), port=12345, smb_kwargs={"debug": 4}) client.open_smbpipe("wkssvc") client.bind(find_dcerpc_interface("wkssvc")) diff --git a/test/scapy/layers/kerberos.uts b/test/scapy/layers/kerberos.uts index 0080964f995..9f6607bb076 100644 --- a/test/scapy/layers/kerberos.uts +++ b/test/scapy/layers/kerberos.uts @@ -32,6 +32,38 @@ assert pkt.root.eData.seq[3].padataValue == b"MIT" etype_info2 = pkt.root.eData.seq[1] assert etype_info2.padataValue.seq[0].salt == b'SAMBA.EXAMPLE.COMhostlocaldc.samba.example.com' += Parse KRB-ERROR (2) + +# This one is a preauth one + +pkt = KerberosTCPHeader(b'\x00\x00\x01A~\x82\x01=0\x82\x019\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x1e\xa4\x11\x18\x0f20251213001046Z\xa5\x05\x02\x03\x05F\x1f\xa6\x03\x02\x01\x19\xa9\x0e\x1b\x0cDOMAIN.LOCAL\xaa!0\x1f\xa0\x03\x02\x01\x02\xa1\x180\x16\x1b\x06krbtgt\x1b\x0cdomain.local\xac\x81\xda\x04\x81\xd70\x81\xd40t\xa1\x03\x02\x01\x13\xa2m\x04k0i0/\xa0\x03\x02\x01\x12\xa1(\x1b&DOMAIN.LOCALhostcomputer1.domain.local0/\xa0\x03\x02\x01\x11\xa1(\x1b&DOMAIN.LOCALhostcomputer1.domain.local0\x05\xa0\x03\x02\x01\x170;\xa1\x03\x02\x01o\xa24\x042000\x0b\x06\t`\x86H\x01e\x03\x04\x02\x030\x0b\x06\t`\x86H\x01e\x03\x04\x02\x020\x0b\x06\t`\x86H\x01e\x03\x04\x02\x010\x07\x06\x05+\x0e\x03\x02\x1a0\t\xa1\x03\x02\x01\x02\xa2\x02\x04\x000\t\xa1\x03\x02\x01\x10\xa2\x02\x04\x000\t\xa1\x03\x02\x01\x0f\xa2\x02\x04\x00') + +assert Kerberos in pkt +assert len(pkt.root.eData.seq) == 5 +assert isinstance(pkt.root.eData.seq[0].padataValue, ETYPE_INFO2) +assert pkt.root.eData.seq[0].padataValue.seq[0].salt.val == b"DOMAIN.LOCALhostcomputer1.domain.local" +assert isinstance(pkt.root.eData.seq[1].padataValue, TD_CMS_DIGEST_ALGORITHMS) +assert [x.algorithm.oidname for x in pkt.root.eData.seq[1].padataValue.seq] == [ + "sha512", + "sha384", + "sha256", + "sha1", +] +assert pkt.root.eData.seq[2].padataType == 2 + += Parse KRB-ERROR (3) + +# This is a TKT EXPIRED + +pkt = KerberosTCPHeader(b'\x00\x00\x00{~y0w\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x1e\xa4\x11\x18\x0f20251213001312Z\xa5\x05\x02\x03\r\xae\x86\xa6\x03\x02\x01 \xa9\x0e\x1b\x0cDOMAIN.LOCAL\xaa!0\x1f\xa0\x03\x02\x01\x02\xa1\x180\x16\x1b\x06krbtgt\x1b\x0cDOMAIN.LOCAL\xac\x19\x04\x170\x15\xa1\x03\x02\x01\x03\xa2\x0e\x04\x0c3\x01\x00\xc0\x00\x00\x00\x00\x01\x00\x00\x00') + +assert Kerberos in pkt +assert pkt.root.errorCode == 0x20 +assert pkt.root.sname.nameString == [b"krbtgt", b"DOMAIN.LOCAL"] +assert isinstance(pkt.root.eData, KERB_ERROR_DATA) +assert pkt.root.eData.dataValue.status == 0xc0000133 +assert pkt.root.eData.dataValue.flags == 1 + = Parse AS-REP pkt = IP(b'E\x00\x05\x95\xff\xff@\x00\xff\x11\x00\x00\x7f\x00\x00\x15\x7f\x00\x00\x15\x00X;p\x05\x81\x00\x00k\x82\x05u0\x82\x05q\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0b\xa2H0F0D\xa1\x03\x02\x01\x13\xa2=\x04;0907\xa0\x03\x02\x01\x12\xa10\x1b.SAMBA.EXAMPLE.COMhostlocaldc.samba.example.com\xa3\x13\x1b\x11SAMBA.EXAMPLE.COM\xa4\x150\x13\xa0\x03\x02\x01\x00\xa1\x0c0\n\x1b\x08LOCALDC$\xa5\x82\x03\xafa\x82\x03\xab0\x82\x03\xa7\xa0\x03\x02\x01\x05\xa1\x13\x1b\x11SAMBA.EXAMPLE.COM\xa2&0$\xa0\x03\x02\x01\x02\xa1\x1d0\x1b\x1b\x06krbtgt\x1b\x11SAMBA.EXAMPLE.COM\xa3\x82\x03a0\x82\x03]\xa0\x03\x02\x01\x12\xa1\x03\x02\x01\x01\xa2\x82\x03O\x04\x82\x03K\t\x05\xd7\x91\xdc\x14\xaa\xe2\xfb\xcc\x85\x1f*?\xbau\xbc0\x0f\x80\x8bc\x87\xe5z\x1a4i\xa3\x9bL[-\xb1\xb7\xaa\xd9-\x01\xc2\xf2\xdfs\x17<\xf3&\x99\'1\xfa\x80\xd9\x02\xae\xf5\xb3S\x14\xc2L\xc3e\xc9\x94\x03dH\xe2\xa9\xfd\x9a\xc6\xffs\x10\xf3er\xbd\xa0\xfep[~\x82+\xde0\x91%tc\xdcx\xfe\xd0\xd8\xc4\xb6u\x91\xe7\xe1C\x00y\xb8\x15\xd9\x91j\x0f\xe7\xa0\xe24m\xd94\xe5.I\xc51\x8f\x1do\t\xe9\x98\xb8\xad\xa6\x92\xf3\x15f\xc98o\x92\x0ch\x08\\\x8f\xab\xfau\xaf\x19v\xcc\xcb!v\xb5v2\xeb(h\x1c+o\xea\xc3\x0b\xcf\x81\xc8\x89\xe8i\xdd?\xd1\xaa\x0f3\xc9\xe9\xf2\xd7\x8a\x93`\x02\x9d\xb2 LV\xda\x0f&>,~\xb3\xecK\xe76v\x9a\xc3\x88\xe3\rj\\/\xd6\x9e_X\x14z\xc2w\x1d.|\xbf\x18\x01\xc8`].\xd2\xc2\x1e\xd0\x89\x8f\xd2\x18\xb9U\xaf\x98\xe9V\xe2\x19\xa1\xbb\xc45\xd9\x16\x08c\xaf$\xef\xf2\xf4S\xeco\xa1\xa1\xe5)\x99\xc9b#[\xd1:O\xbej\xb91\xb3i\xbepb\x06\xd8\x14\xc3\xdf\xbb\x18\xbf]\xf1\x82+\x18*\x85D\xecy\x0eu_\xe2\xfa\xbcd\x82A>\x88p\xa2\xc1\xf6\x9c\x89Qj\xfdM\x99\xd1\x84r\x0fp\x06$\xab\xc2\xb5\xae4\xe8\xf1\xbb}\x98\xedWX\xe2*uB\x93\x11\x1c\xc7f\x1c\xce\xc9\xff\t\x88\x94\xddN\xcf\xa68O\x0c^I\x9ew\x81\xba\xc3\xbc\xa8\x07\x8b\xd4\xdf\x7f(\xc2\x15gX\xd0oN\x00u\x1aU@\xbd\xb8\xa9)Ur\x94\xc1\xcf\xa1\xd8k\xc1F\x19\xd3rR\xaa\x93\xe2\x06D#\x12\x07M\xe3\x15\xd6\xd0\xb3\xa6\x89\x0c\xfeLO6\xe6\xf0w\x1a\x80\x0f\xffO\xf2N\xf4(\n\xdb-\x96`\xa4\xb7\xd3g\x16\xbfY\xff\xad\x95\x19\xd9\x9cS\xaa\xe3\x06W\xf3\xc2\x18it5\xda\x1c\x99\x8a\xaf\xfa"MT\xc7$#j,P\x9b\xf9\r\xbbA\xd0w\x15.\xc3PC\xc4\xe7vL/\xca0h7\x1c4z\x8bS@\x0ej\xb4q\xde\x19\xd8so\x9c\xea\x8f^w7\x1e\x92\x1c\xcc\xe2\xa60\xe8\xce}\xee\xb1\x87F!n\x80\xe4l"\xed\xc2fI \xb9\t\x14\t\x8d\xect\xa4\xb48\xe0\xfd\xf3\xe5\x8es\xd2\x08;\x9f\xb2\xb8q\x1bX\xadd\xbb\x07z\x16\tZ\xb0z1+h\x0e\xf7\x98w\x0bX\xf0W\t\xa6\x86.\x1e\x9c\xc2\x9d\xac+\xca\xdf&\xa9\xf3\xcb\xa7\xca\x1fn\xe8\x8a]h\xf6\xeb\xe9\xd4\xa0\x16\x1b\xb4\x8d\xc7\xaf\xe3\xf0.\x85\x1e\xc2\xa5\xf2DhhgQ\xe0\xb8y\xb8\xbd\x98\xf8\xa0\rW\x93/\x07>0\xf5\x92Y\x15Y\x0bD\xdb\xd6\xac#\xd8z\xbdeY\x87\xf2\x97\xfdZ\x0c\x1d\xbc\xefXONv\xc9\xfdp\xdd^\x16\x83\xc3\xeb\x9e\x96+\xe8\xed\x0c<$\x83A\xeb\xc6e\x94\x0c\x11\x19\xb4\x99\xcd\x17\xeb\xcb.\x0b}\x01i\x88\x03R\xde\x1a\xea\x03\x10\xa9Z\x8e\xf7\x87\r\xa6\x08@\xf7\x96\xc8\xa5g\xde\x8dE\xf8\xb0\xe8\xe6T\x80=\x0cm\xe0z\xa5\x03\xa2X\xed\'\x17\x001O\xee\xfb\x87\xbe\xf7\xbbS\xc1p\xaeZ\x17\x92}\xc2\x07\x01\x81\xaew\xd9\xc5\x9c\xe5k\x8d+\x13\xd2\x00Q\xd4\xe5M\x9d\x06\xc7)\xac\x06\xb2+\xd1\x83\xcb\xfe\xb9\xf9\x0bbRN\x04\xe7\xd8\xa0\xf9\xe3\xc3m\x18\xc4\x108\xfa\xa6\x82\x01:0\x82\x016\xa0\x03\x02\x01\x12\xa2\x82\x01-\x04\x82\x01)/pDi\x13\xee\x0b\x8ehN2\x01P\x19|\xda\x1a\xde\xec\xde\rt\xcbe7\x00-sG&\x8b\xfc\xa4\x92~~[,\xd5\rAj\xd6[\xbe\xeeB\xf8X\\x\xa6$Z\x83\xf6\x1bq\xc5\x8fm\\\x94\xd7l\xc5\x89#\xcb\xcd\xaf\xff\x15\x1b\x8f;7\xb0\xc8u\x19\xb1\xd0\xb0\x93\xa7z\x9cz\x14\x0b\x86q\x01\xb8<\xa7\xa4\xceb\x1f\x88\x14\xe3S0\xe3]\xa5\x9b\xa0\x0e\x97#\x87\x9a\xe0\x90a\xdfj.\x1e6x\x87GV\xc0/\xa4\xab}\xdbS\xd5\xff\x03\x03\xae\x18n\x1aQ\r\x7fP\xdb\xfe\xe9\xeb\xab2\x9dws9\xf5\xcb\x94\xab\xc1\x9e\xbd\x08\x0f\xfcx\x18\x1b\xf8\x1f\xf2\'\x18-\xe4"\x93vuTf3\xbdj\xb6\x88%\x8a\x94\xd12\xfbY\x0f\x81R\xd3\xf1\x9b\xd5Z\x1f3o\xb7\xc3\x82\x14\t\x87\xac#\x89\x13M\x803\x88/\x92==S$\xa3\xe9\xf5C{\xd7\x0f\t^k\xb0\x0e\xe6\x8d\x8f!\x91+\x19\xb2y$\xc6\x1bN;\xfehA\x1f\x9f"\r\xe8\xda\xce\x00\xe7g\xb6b17\x06s\rM\xc8S\x9b0\x9f\xc7^l\xa4\xca\xe4p\xcd\xf1,\xc3\xcf\xb1\x91Hn>^\xb8\xc8\x07#\xb2\xb0G;\x07\xe4\xeaM8T\x87\xdd0=\xf2\xdb\x8d1\xf8\xc9\rS\xc4\xad\xcf9\xadx\xcfl\x85\xfb\xb8{LN\xe51\xa4,!3\xdf+\x03b\x13#t\xdf\x99T \xe4\xb2\xa6\xd1\xe1\x9dxy\xd5\x18e-Q\x01\xa3\x16\x96+\'\xb3\x88L\xb6}\x07W/\x96\xb9f\x8c\xa4,\xcas\x11\xa7\x15*\xc7\xc6\xd4\x92\x00\x91\x92\xfaJpy\x89\xe4;*\x10\xf1\x9eS^|\xf8\xaf\xda\xf6<\xe9\xa2\xa8\\\xe1\xbd\x17\xd8\x1c\xfev\xd2\xceWY\xa7\xfd\xbe\xffo\xb2y\xb8b\x0b\xc2\xc5\x18;$\xbe\x83\x1c~\xe1W\x11O\'\x00\xda!\x0b6\xed\xb7\xbd\xa7\xd9\x1a2\xf7\x94\x0b\xefC\x1cvW\x1c\xd4D\x99\xf7y\xccN\xbe\x82\x9f\xb3N\xea\xa1\xe4B$\rYb\xbd\xbc\xbc\x16\xc9b\x97KTn\x9c\xee8\r\xdaI\xf6Q\xac\xc5\xc5\x8a\xca\xe4\xad\x06\xd5~K\x91\xd8\xc5Use\xe8\xdd\xda~\xe9U\tc\xd7\rOV\xb4O\xc5\xa2n)\xb3l\xb2\x1d\x11"\x18%\xb5\xa2!|\xb1\xf1EM4\xd9J\x85\\\xb8`\xf2\xfeCh\x1e=0.~\x12Bs\xdd\x18\xb0O\xdd\xf6`\xb8\x85\x8e\x1ex\xd0"\xcc\x03\xf4g\xf3\xcf\x1an]\xf5;\xb81yEB\xb1\xd0\x8e8\xd3\xbf\xb0\xbf.[\xa6\xf7Z\x0fw\xd5k\xf2\x92K\x14O\xff<\x87\xeczW\xbf\xf3E\xee\x8aD\x96gm8\xc9E<8\xe6E!\xdb-\xe6\xd6E*\xa8\xf3\xda\x16u\x13N\x8d\x90\xcb\xb0\xd2t\xcea\x89V?\xd9\xa5nV\xa8\x00f\x1ex{\x089Pb05\xdd\xee\xb2\xfb\x84\xf6\xfb%\x07\xf2\xc1W\xe7N\x81\xa8\x19p\xe1\x14u\xce\x92n9:U\xb0kw\xc4D\xdb\xd26\x88\xe8\xa7|\x7f03xt\xfe\xf7\x87\xa1\x87\xfc\xaf\xd7:ZH7\xc8\xe3\xe6\x07\x120\x85\x97\xffr\xea.\xda\xe6\x9c\x94\x02\xadz\xe8\x1a\xbb>\x91\x00\xf0\xc8{\x99\xb2VBF\xbdV\xaf\x8em\x0e\xcf)(\xe5\x15\x12\x18\xf7\xe6\'\xc5e\xe1U@foO|\x0e\x93|-\x0e\x84x/\xcb\x1bS^YolN\n\xed|\x1d5\x0e\x16\x9d\x04_.\xaa\xa4\xbb/\x94\xcd\x14\x95v\xf85\xe5\xee\xcbD\x18g}\x04D\xe5\x1f\xaf\xcb\xed*\xfa\xc5\x0b\x1d2\x0b\xc2#\xd2b6\x01\xae\xe6\xdfj6:$)K\xfb;\x00\xf2f\x8d\xfc@N\x9f\xa1\x7f\xe96\xe6b\x07V\xa6\x91\x8f}\xe2\xde4?8\x0f\xab\x83\xfd\xe9\x11\x12K\xe5\x08\xa4\x82\x01\\0\x82\x01X\xa0\x03\x02\x01\x12\xa2\x82\x01O\x04\x82\x01K\\>\t\xe4\x1d8,a(\x7f\x1e\xd2\x8dHH\x9c\xa3\x03?&\xb9\xf4\xba\xef\xcf\xcf\xb6(8\x91\x0f\xa3lq\xc6 f&Ou\xd8Bk\xe84s\xf1\xec\xf6\x97wY\xc6Un;\xf5\xdeh\xb9J\xd6\xaf\xf4r\x00\x80\x17\x8d\xc4p\x81\xac\x89\xf1\xf6\x98\xef\x1f\xb3\xe5\x91}\xf5m\x1a\xbd\x08\x1d\x0217W0\x81\xddZ\xec,J%\xe2o\x86\xef{"a\xe0\xe2hBc\xeb^\x8b\xa3\x8c\xf7W\xf9F\xc6&\x1a\x041\x0c\xdf\xc3S\xaa>\x04\x90\xd7\x8a\xdd\xf3j\x80#4_\x95u\xaby3\x0f\x878\xe3\',t\xa7\xe9\xba7&\xd6\x82y\x1d9\x06\xf1\xff\xaf\xb33O\xdb\x00\xc5\x19\xd0\xb7\t\xe9\xeb\xe0iv\x08\xaa\xf4\x00\xcaG\xbb7\xb9P\xcd\xcf\xcbC\x9b\xec\xfdH\x1b\xbf\x89\x11\x96L\xa8\xb4\\6\xcf\x9a\xa6\x16\xf0\xfb,\xaf\x06.qj\xf0\x03\xfd\xc0 \x80\xb6\xb84\xcf\xec\tW~5\xad,\x14-\xf05\x04\xb2\xd4[o\xce\xa3\xf9\x06\x08\x0e\xeb\x1e\xbf2\xd7\xe4\xc2\x14\xabn_\x0c8j;#\r\xee\xce\xa6\x1f\xc3+\xed\x0c\xb7\xabdb\xb4\x8b\xb2\xd0\xe97\xa5P\xcd\xf1\x96\x8aT:=\xfc\xd9\x1e\xb6q\xcdM\x16\xead\x81\x84/\xab\xdd\xc8\xe1\xed\x17\xa3\xf5\x1c\xf1\x98\xf1\xf7\xbd\xbc\xc8\xdf' + = GSS_Accept_sec_context (SPNEGO_negTokenResp: KRB_AP_REQ->KRB_AP_REP) with KrbRandomPatcher(): - srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) + srvcontext, tok, negState = server.GSS_Accept_sec_context(None, tok) -assert negResult == 0 +assert negState == 0 assert isinstance(tok, SPNEGO_negToken) tok = SPNEGO_negToken(bytes(tok)) assert isinstance(tok.token, SPNEGO_negTokenResp) -assert tok.token.negResult == 0 +assert tok.token.negState == 0 assert tok.token.supportedMech.oid == '1.2.840.48018.1.2.2' assert isinstance(tok.token.responseToken, SPNEGO_Token) -assert tok.token.mechListMIC is not None +assert tok.token.mechListMIC is None ap_rep = tok.token.responseToken.value.root assert isinstance(ap_rep, KRB_AP_REP) @@ -1478,15 +1647,15 @@ assert apreppart.subkey.keytype == 17 # Hardcode (yes this will probably require updating this test) bytes(tok) -assert bytes(tok) == b'\xa1\x81\xa90\x81\xa6\xa0\x03\n\x01\x00\xa1\x0b\x06\t*\x86H\x82\xf7\x12\x01\x02\x02\xa2r\x04pon0l\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0f\xa2`0^\xa0\x03\x02\x01\x12\xa2W\x04UaS\xeck\xcc\xad~\xfa^\x8d\xca\xbb\xc5\xd2/\xfd\xd3\xc3\xd9\xadN`\xd2;\xd7{\xb7\xf4p.\xa9\x9a\xb1}D\xc6|_t\n\r"M\xcd\xe2\t\xf0Ri\xc7\xcf\xb5\xefr9\xf0`iS7N\x06qKP\x06\xde\xc4\x18\xd5_\xcb\x0ct\x03k\xbc\xb9\x1adT\x03\xc1\x8bM\xa3\x1e\x04\x1c\x04\x04\x05\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x17F\x8al\x01c\x00\xcf4\x12oI' +assert bytes(tok) == b'\xa1\x81\x890\x81\x86\xa0\x03\n\x01\x00\xa1\x0b\x06\t*\x86H\x82\xf7\x12\x01\x02\x02\xa2r\x04pon0l\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0f\xa2`0^\xa0\x03\x02\x01\x12\xa2W\x04UaS\xeck\xcc\xad~\xfa^\x8d\xca\xbb\xc5\xd2/\xfd\xd3\xc3\xd9\xadN`\xd2;\xd7{\xb7\xf4p.\xa9\x9a\xb1}D\xc6|_t\n\r"M\xcd\xe2\t\xf0Ri\xc7\xcf\xb5\xefr9\xf0`iS7N\x06qKP\x06\xde\xc4\x18\xd5_\xcb\x0ct\x03k\xbc\xb9\x1adT\x03\xc1\x8bM' = GSS_Init_sec_context (SPNEGO_negToken: KRB_AP_REP->OK) with KrbRandomPatcher(): - clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) + clicontext, tok, negState = client.GSS_Init_sec_context(clicontext, tok) assert tok is None -assert negResult == 0 +assert negState == 0 assert clicontext.KrbSessionKey.key == srvcontext.KrbSessionKey.key assert srvcontext.KrbSessionKey.key == b'0000000000000000' @@ -1524,8 +1693,8 @@ sig = server.GSS_GetMICEx( ] ) assert isinstance(sig, KRB_InnerToken) and sig.TOK_ID == b"\x04\x04" -assert sig.root.SND_SEQ == 1 -assert bytes(sig) == b'\x04\x04\x05\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x01G\x81\x93\xb9\x92\xd0NvHH\xf6\x9c' +assert sig.root.SND_SEQ == 0 +assert bytes(sig) == b'\x04\x04\x05\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x98\xdeb<\x14\x1c\x9fe%{\x0e\xf7' client.GSS_VerifyMICEx( clicontext, [ @@ -1575,7 +1744,7 @@ server = KerberosSSP( = GSS_Init_sec_context (KRB_AP_REQ) - DCE_STYLE with KrbRandomPatcher(): - clicontext, tok, negResult = client.GSS_Init_sec_context( + clicontext, tok, negState = client.GSS_Init_sec_context( None, req_flags=( GSS_C_FLAGS.GSS_C_DCE_STYLE | @@ -1586,7 +1755,7 @@ with KrbRandomPatcher(): ) ) -assert negResult == 1 +assert negState == 1 assert isinstance(tok, KRB_AP_REQ) ap_req = KRB_AP_REQ(bytes(tok)) assert isinstance(ap_req, KRB_AP_REQ) @@ -1611,9 +1780,9 @@ assert bytes(tok) == b'n\x82\x06\x1d0\x82\x06\x19\xa0\x03\x02\x01\x05\xa1\x03\x0 = GSS_Accept_sec_context (KRB_AP_REQ->KRB_AP_REP) - DCE_STYLE with KrbRandomPatcher(): - srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) + srvcontext, tok, negState = server.GSS_Accept_sec_context(None, tok) -assert negResult == 1 +assert negState == 1 assert isinstance(tok, KRB_AP_REP) ap_rep = KRB_AP_REP(bytes(tok)) @@ -1629,9 +1798,9 @@ assert bytes(tok) == b'on0l\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0f\xa2`0^\xa0\x = GSS_Init_sec_context (SPNEGO_negToken: KRB_AP_REP->KRB_AP_REP) - DCE_STYLE with KrbRandomPatcher(): - clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) + clicontext, tok, negState = client.GSS_Init_sec_context(clicontext, tok) -assert negResult == 0 +assert negState == 0 assert isinstance(tok, KRB_AP_REP) ap_rep = KRB_AP_REP(bytes(tok)) @@ -1645,9 +1814,9 @@ assert bytes(tok) == b'oQ0O\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0f\xa2C0A\xa0\x = GSS_Accept_sec_context (KRB_AP_REP->OK) - DCE_STYLE with KrbRandomPatcher(): - srvcontext, tok, negResult = server.GSS_Accept_sec_context(srvcontext, tok) + srvcontext, tok, negState = server.GSS_Accept_sec_context(srvcontext, tok) -assert negResult == 0 +assert negState == 0 assert tok is None diff --git a/test/scapy/layers/msnrpc.uts b/test/scapy/layers/msnrpc.uts index 7b31bf85421..f678d7f6274 100644 --- a/test/scapy/layers/msnrpc.uts +++ b/test/scapy/layers/msnrpc.uts @@ -40,7 +40,7 @@ EncryptedMessage = bytes.fromhex("c930c9a079d95c78bea6a3150908c11f4b68e41219bcb9 # Perform the same operation using NetlogonSSP: client = NetlogonSSP(SessionKey=SessionKey, computername="DC1", domainname="DOMAIN", AES=True) -clicontext, tok, negResult = client.GSS_Init_sec_context(None) +clicontext, tok, negState = client.GSS_Init_sec_context(None) with mock.patch('scapy.layers.msrpce.msnrpc.os.urandom', side_effect=lambda x: Confounder): _msgs, sig = client.GSS_WrapEx( @@ -67,7 +67,7 @@ FullNetlogonSignatureHeader = bytes.fromhex("13001a00ffff00005d69950dfde45ae9f09 # Perform the same operation using NetlogonSSP: client = NetlogonSSP(SessionKey=SessionKey, computername="DC1", domainname="DOMAIN", AES=True) -clicontext, tok, negResult = client.GSS_Init_sec_context(None) +clicontext, tok, negState = client.GSS_Init_sec_context(None) with mock.patch('scapy.layers.msrpce.msnrpc.os.urandom', side_effect=lambda x: Confounder): _msgs, sig = client.GSS_WrapEx( @@ -287,9 +287,9 @@ server = NetlogonSSP(SessionKey=b"\x00\x00\x00\x00\x00\x00\x00\x00", computernam = [NetlogonSSP] - RC4 - GSS_Init_sec_context (NL_AUTH_MESSAGE) -clicontext, tok, negResult = client.GSS_Init_sec_context(None) +clicontext, tok, negState = client.GSS_Init_sec_context(None) -assert negResult == 1 +assert negState == 1 assert isinstance(tok, NL_AUTH_MESSAGE) assert tok.MessageType == 0 assert tok.Flags == 3 @@ -299,9 +299,9 @@ assert bytes(tok) == b'\x00\x00\x00\x00\x03\x00\x00\x00DOMAIN\x00DC1\x00' = [NetlogonSSP] - RC4 - GSS_Accept_sec_context (NL_AUTH_MESSAGE->NL_AUTH_MESSAGE) -srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) +srvcontext, tok, negState = server.GSS_Accept_sec_context(None, tok) -assert negResult == 0 +assert negState == 0 assert tok.MessageType == 1 bytes(tok) @@ -309,9 +309,9 @@ assert bytes(tok) == b'\x01\x00\x00\x00\x00\x00\x00\x00' = [NetlogonSSP] - RC4 - GSS_Init_sec_context (NL_AUTH_MESSAGE->OK) -clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) +clicontext, tok, negState = client.GSS_Init_sec_context(clicontext, tok) -assert negResult == 0 +assert negState == 0 assert tok is None = [NetlogonSSP] - RC4 - GSS_WrapEx/GSS_UnwrapEx: client sends a encrypted payload @@ -400,9 +400,9 @@ server = NetlogonSSP(SessionKey=b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x = [NetlogonSSP] - AES - GSS_Init_sec_context (NL_AUTH_MESSAGE) -clicontext, tok, negResult = client.GSS_Init_sec_context(None) +clicontext, tok, negState = client.GSS_Init_sec_context(None) -assert negResult == 1 +assert negState == 1 assert isinstance(tok, NL_AUTH_MESSAGE) assert tok.MessageType == 0 assert tok.Flags == 3 @@ -412,9 +412,9 @@ assert bytes(tok) == b'\x00\x00\x00\x00\x03\x00\x00\x00DOMAIN\x00DC1\x00' = [NetlogonSSP] - AES - GSS_Accept_sec_context (NL_AUTH_MESSAGE->NL_AUTH_MESSAGE) -srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) +srvcontext, tok, negState = server.GSS_Accept_sec_context(None, tok) -assert negResult == 0 +assert negState == 0 assert tok.MessageType == 1 bytes(tok) @@ -422,9 +422,9 @@ assert bytes(tok) == b'\x01\x00\x00\x00\x00\x00\x00\x00' = [NetlogonSSP] - AES - GSS_Init_sec_context (NL_AUTH_MESSAGE->OK) -clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) +clicontext, tok, negState = client.GSS_Init_sec_context(clicontext, tok) -assert negResult == 0 +assert negState == 0 assert tok is None = [NetlogonSSP] - AES - GSS_WrapEx/GSS_UnwrapEx: client sends a encrypted payload diff --git a/test/scapy/layers/ntlm.uts b/test/scapy/layers/ntlm.uts index 83b66197297..8bc38dceb66 100644 --- a/test/scapy/layers/ntlm.uts +++ b/test/scapy/layers/ntlm.uts @@ -158,7 +158,7 @@ server = SPNEGOSSP([ = GSS_Init_sec_context (negTokenInit: NTLM_NEGOTIATE) -clicontext, tok, negResult = client.GSS_Init_sec_context( +clicontext, tok, negState = client.GSS_Init_sec_context( None, req_flags=( GSS_C_FLAGS.GSS_C_MUTUAL_FLAG | @@ -166,7 +166,7 @@ clicontext, tok, negResult = client.GSS_Init_sec_context( GSS_C_FLAGS.GSS_C_CONF_FLAG ) ) -assert negResult == 1 +assert negState == 1 assert isinstance(tok, GSSAPI_BLOB) tok = GSSAPI_BLOB(bytes(tok)) assert tok.MechType.val == '1.3.6.1.5.5.2' @@ -192,13 +192,13 @@ assert bytes(ntlm_nego) == b'NTLMSSP\x00\x01\x00\x00\x005\x82\x89\xe2\x00\x00\x0 = GSS_Accept_sec_context (SPNEGO_negTokenResp: NTLM_NEGOTIATE->NTLM_CHALLENGE) with NTLMRandomPatcher(): - srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) + srvcontext, tok, negState = server.GSS_Accept_sec_context(None, tok) -assert negResult == 1 +assert negState == 1 assert isinstance(tok, SPNEGO_negToken) tok = SPNEGO_negToken(bytes(tok)) assert isinstance(tok.token, SPNEGO_negTokenResp) -assert tok.token.negResult == 1 +assert tok.token.negState == 1 assert tok.token.supportedMech.oid == '1.3.6.1.4.1.311.2.2.10' assert isinstance(tok.token.responseToken, SPNEGO_Token) assert tok.token.mechListMIC is None @@ -216,41 +216,42 @@ assert ntlm_chall.getAv(0) = GSS_Init_sec_context (SPNEGO_negToken: NTLM_CHALLENGE->NTLM_AUTHENTICATE) with NTLMRandomPatcher(): - clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) + clicontext, tok, negState = client.GSS_Init_sec_context(clicontext, tok) assert isinstance(tok, SPNEGO_negToken) tok = SPNEGO_negToken(bytes(tok)) assert isinstance(tok.token, SPNEGO_negTokenResp) -assert tok.token.negResult is None +assert tok.token.negState is None assert tok.token.supportedMech is None assert isinstance(tok.token.mechListMIC, SPNEGO_MechListMIC) -sig = NTLMSSP_MESSAGE_SIGNATURE(tok.token.mechListMIC.value.val) +sig = tok.token.mechListMIC.value +assert isinstance(sig, NTLMSSP_MESSAGE_SIGNATURE) assert sig.Version == 1 assert sig.SeqNum == 0 assert isinstance(tok.token.responseToken, SPNEGO_Token) -ntlm_auth = NTLM_Header(tok.token.responseToken.value.val) +ntlm_auth = tok.token.responseToken.value assert isinstance(ntlm_auth, NTLM_AUTHENTICATE_V2) assert ntlm_auth.NegotiateFlags == 0xe2898235 assert ntlm_auth.UserName == "User1" assert ntlm_auth.DomainName == "DOMAIN" assert ntlm_auth.Workstation == "WIN10" assert ntlm_chall.TargetInfo[:6] == ntlm_auth.NtChallengeResponse.AvPairs[:6] -assert ntlm_auth.NtChallengeResponse.TimeStamp == ntlm_chall.getAv(7).Value assert ntlm_auth.NtChallengeResponse.getAv(6).Value == 2 assert ntlm_auth.NtChallengeResponse.getAv(9).Value == "host/WIN10" = GSS_Accept_sec_context (SPNEGO_negToken: NTLM_AUTHENTICATE->OK) -srvcontext, tok, negResult = server.GSS_Accept_sec_context(srvcontext, tok) -assert negResult == 0 # success :p +srvcontext, tok, negState = server.GSS_Accept_sec_context(srvcontext, tok) +assert negState == 0, negState # success :p assert isinstance(tok, SPNEGO_negToken) assert isinstance(tok.token, SPNEGO_negTokenResp) -assert tok.token.negResult == 0 +assert tok.token.negState == 0 assert tok.token.supportedMech is None assert tok.token.responseToken is None assert isinstance(tok.token.mechListMIC, SPNEGO_MechListMIC) -sig = NTLMSSP_MESSAGE_SIGNATURE(tok.token.mechListMIC.value.val) +sig = tok.token.mechListMIC.value +assert isinstance(sig, NTLMSSP_MESSAGE_SIGNATURE) assert sig.Version == 1 assert sig.SeqNum == 0 @@ -390,7 +391,6 @@ server = SPNEGOSSP( }, ), ], - force_supported_mechtypes=tok0.innerToken.token.mechTypes ) = Real exchange - Parse token 1 from client @@ -402,8 +402,8 @@ b"\x04\x28\x4e\x54\x4c\x4d\x53\x53\x50\x00\x01\x00\x00\x00\x97\x82" \ b"\x08\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ b"\x00\x00\x0a\x00\x61\x4a\x00\x00\x00\x0f") -srvcontext, _, negResult = server.GSS_Accept_sec_context(None, tok1) -assert negResult == 1 +srvcontext, _, negState = server.GSS_Accept_sec_context(None, tok1) +assert negState == 1 = Real exchange - Inject token 2 from server @@ -425,7 +425,7 @@ b"\x00\x02\xea\x8e\xe8\xd2\x8d\xd9\x01\x00\x00\x00\x00") tok2.token.responseToken.value.show() # Inject challenge token -srvcontext.sub_context.chall_tok = tok2.token.responseToken.value +srvcontext.ssp_context.chall_tok = tok2.token.responseToken.value = Real exchange - Parse token 3 from client @@ -462,8 +462,8 @@ b"\x47\xdc\xcd\xb5\x5e\x13\x62\xa3\x12\x04\x10\x01\x00\x00\x00\x0f" \ b"\x96\x54\xbb\x55\xd0\x6c\xcb\x00\x00\x00\x00") # Parse auth -srvcontext, tok, negResult = server.GSS_Accept_sec_context(srvcontext, tok3) -assert negResult == 0 +srvcontext, tok, negState = server.GSS_Accept_sec_context(srvcontext, tok3) +assert negState == 0 = Real exchange - Check mechListMIC against token 4 from server diff --git a/test/scapy/layers/smb.uts b/test/scapy/layers/smb.uts index 6afcd4e9c60..1b90f1c348d 100644 --- a/test/scapy/layers/smb.uts +++ b/test/scapy/layers/smb.uts @@ -105,7 +105,7 @@ from scapy.layers.ntlm import * smb_sax_resp_1 = Ether(b"\x00\x0c)a\xf5_\x00PV\xc0\x00\x01\x08\x00E\x00\x01,\x03I@\x00\x80\x06\xe6\xaa\xc0\xa8\xc7\x01\xc0\xa8\xc7\x85\x00\x8b\xc2\x08\x10]}F\xd7\xcb\xefiP\x18\x00\xff\xeb)\x00\x00\x00\x00\x01\x00\xffSMBs\x16\x00\x00\xc0\x98\x07\xc8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xfe\x00\x08\x10\x00\x04\xff\x00\x00\x01\x00\x00\x93\x00\xd5\x00\xa1\x81\x900\x81\x8d\xa0\x03\n\x01\x01\xa1\x0c\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\xa2x\x04vNTLMSSP\x00\x02\x00\x00\x00\x06\x00\x06\x008\x00\x00\x00\x15\x82\x8a\xe2\x88\xbc\x9bX4\xbe7\r\x00\x00\x00\x00\x00\x00\x00\x008\x008\x00>\x00\x00\x00\x06\x03\x80%\x00\x00\x00\x0fS\x00C\x00V\x00\x02\x00\x06\x00S\x00C\x00V\x00\x01\x00\x06\x00S\x00C\x00V\x00\x04\x00\x06\x00S\x00C\x00V\x00\x03\x00\x06\x00S\x00C\x00V\x00\x07\x00\x08\x00\xd5\x9d6\x9b\x84'\xd2\x01\x00\x00\x00\x00W\x00i\x00n\x00d\x00o\x00w\x00s\x00 \x008\x00.\x001\x00 \x009\x006\x000\x000\x00\x00\x00W\x00i\x00n\x00d\x00o\x00w\x00s\x00 \x008\x00.\x001\x00 \x006\x00.\x003\x00\x00\x00") assert SMBSession_Setup_AndX_Response_Extended_Security in smb_sax_resp_1 assert smb_sax_resp_1.AndXCommand == 255 -assert smb_sax_resp_1.SecurityBlob.token.negResult == 1 +assert smb_sax_resp_1.SecurityBlob.token.negState == 1 assert isinstance(smb_sax_resp_1.SecurityBlob.token.responseToken.value, NTLM_CHALLENGE) ntlm_challenge = smb_sax_resp_1.SecurityBlob.token.responseToken.value assert len(ntlm_challenge.Payload) == 2 @@ -130,8 +130,8 @@ assert SMBSession_Setup_AndX_Request_Extended_Security in smb_sax_req_2 assert smb_sax_req_2.Flags2.EXTENDED_SECURITY assert smb_sax_req_2.Flags2.UNICODE assert smb_sax_req_2.AndXCommand == 255 -assert smb_sax_req_2.SecurityBlob.token.negResult == 1 -ntlm_authenticate = NTLM_Header(smb_sax_req_2.SecurityBlob.token.responseToken.value.val) +assert smb_sax_req_2.SecurityBlob.token.negState == 1 +ntlm_authenticate = smb_sax_req_2.SecurityBlob.token.responseToken.value assert isinstance(ntlm_authenticate, NTLM_AUTHENTICATE) assert len(ntlm_authenticate.Payload) == 3 assert ntlm_authenticate.Payload[0] == ('Workstation', 'DESKTOP-V1FA0UQ') @@ -144,8 +144,10 @@ assert ntlm_authenticate.Payload[2][1] == b'/\t\x13+\x81\xa6\x15\x14\xb9\x11\x8b smb_sax_resp_2 = Ether(b'\x00\x0c)a\xf5_\x00PV\xc0\x00\x01\x08\x00E\x00\x00\xb6\x03J@\x00\x80\x06\xe7\x1f\xc0\xa8\xc7\x01\xc0\xa8\xc7\x85\x00\x8b\xc2\x08\x10]~J\xd7\xcb\xf0YP\x18\x00\xfeB\x10\x00\x00\x00\x00\x00\x8a\xffSMBs\x00\x00\x00\x00\x98\x07\xc8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xfe\x00\x08 \x00\x04\xff\x00\x8a\x00\x00\x00\x1d\x00_\x00\xa1\x1b0\x19\xa0\x03\n\x01\x00\xa3\x12\x04\x10\x01\x00\x00\x00\xee\t\x91S\xab\x7f]\xe6\x00\x00\x00\x00W\x00i\x00n\x00d\x00o\x00w\x00s\x00 \x008\x00.\x001\x00 \x009\x006\x000\x000\x00\x00\x00W\x00i\x00n\x00d\x00o\x00w\x00s\x00 \x008\x00.\x001\x00 \x006\x00.\x003\x00\x00\x00') assert SMBSession_Setup_AndX_Response_Extended_Security in smb_sax_resp_2 -assert smb_sax_resp_2.SecurityBlob.token.negResult == 0 -assert smb_sax_resp_2.SecurityBlob.token.mechListMIC.value.val == b'\x01\x00\x00\x00\xee\t\x91S\xab\x7f]\xe6\x00\x00\x00\x00' +assert smb_sax_resp_2.SecurityBlob.token.negState == 0 +assert smb_sax_resp_2.SecurityBlob.token.mechListMIC.value.Version == 1 +assert smb_sax_resp_2.SecurityBlob.token.mechListMIC.value.Checksum == b'\xee\t\x91S\xab\x7f]\xe6' +assert smb_sax_resp_2.SecurityBlob.token.mechListMIC.value.SeqNum == 0 assert smb_sax_resp_2.NativeOS == 'Windows 8.1 9600' assert smb_sax_resp_2.NativeLanMan == 'Windows 8.1 6.3' diff --git a/test/scapy/layers/spnego.uts b/test/scapy/layers/spnego.uts new file mode 100644 index 00000000000..ff04b448091 --- /dev/null +++ b/test/scapy/layers/spnego.uts @@ -0,0 +1,317 @@ +% SPNEGO unit tests + ++ Special SPNEGO tests + += SPNEGOSSP - test raw fallback + +% A SPNEGOSSP server talking to a non SPNEGOSSP client should work. + +srvssp = SPNEGOSSP([KerberosSSP(), NTLMSSP(IDENTITIES={"User1": MD4le("Password123!")})]) +clissp = NTLMSSP(UPN="User1", PASSWORD="Password123!") + +clictx, tok, status = clissp.GSS_Init_sec_context(None) +assert status == GSS_S_CONTINUE_NEEDED, status +srvctx, tok, status = srvssp.GSS_Accept_sec_context(None, tok) +assert status == GSS_S_CONTINUE_NEEDED, status +clictx, tok, status = clissp.GSS_Init_sec_context(clictx, tok) +assert status == GSS_S_COMPLETE, status +srvctx, tok, status = srvssp.GSS_Accept_sec_context(srvctx, tok) +assert status == GSS_S_COMPLETE, status +assert tok is None, repr(tok) + += SPNEGOSSP - SSP negotiation + mechListMIC + +% Two SPNEGOSSPs with different preferred mechanisms should work, +% and mechListMIC should be used. + +srvssp = SPNEGOSSP([ + KerberosSSP(), + NTLMSSP(IDENTITIES={"User1": MD4le("Password123!")}) +]) +clissp = SPNEGOSSP([ + NTLMSSP(UPN="User1", PASSWORD="Password123!"), +]) + +clictx, tok, status = clissp.GSS_Init_sec_context(None) +assert clictx.require_mic +assert status == GSS_S_CONTINUE_NEEDED, status +assert len(tok.innerToken.token.mechTypes) == 1 +assert tok.innerToken.token.mechTypes[0].oid.val == '1.3.6.1.4.1.311.2.2.10' +assert tok.innerToken.token.mechListMIC is None +assert tok.innerToken.token._mechListMIC is None +assert isinstance(tok.innerToken.token.mechToken.value, NTLM_NEGOTIATE) + +srvctx, tok, status = srvssp.GSS_Accept_sec_context(None, tok) +assert srvctx.require_mic +assert status == GSS_S_CONTINUE_NEEDED, status +assert tok.token.mechListMIC is None +assert tok.token.negState == 1 +assert tok.token.supportedMech.oid.val == '1.3.6.1.4.1.311.2.2.10' +assert isinstance(tok.token.responseToken.value, NTLM_CHALLENGE) + +clictx, tok, status = clissp.GSS_Init_sec_context(clictx, tok) +assert status == GSS_S_CONTINUE_NEEDED, status +assert tok.token.negState is None +assert tok.token.supportedMech is None +assert isinstance(tok.token.responseToken.value, NTLM_AUTHENTICATE) +assert isinstance(tok.token.mechListMIC.value, NTLMSSP_MESSAGE_SIGNATURE) +assert tok.token.mechListMIC.value.SeqNum == 0 +assert tok.token.mechListMIC.value.Version == 1 + +srvctx, tok, status = srvssp.GSS_Accept_sec_context(srvctx, tok) +assert status == GSS_S_COMPLETE, status +assert tok is not None +assert isinstance(tok.token, SPNEGO_negTokenResp) +assert isinstance(tok.token.mechListMIC.value, NTLMSSP_MESSAGE_SIGNATURE) +assert tok.token.mechListMIC.value.Version == 1 +assert tok.token.mechListMIC.value.SeqNum == 0 + +clictx, tok, status = clissp.GSS_Init_sec_context(clictx, tok) +assert status == GSS_S_COMPLETE, status +assert tok is None + += SPNEGOSSP - SSP negotiation + mechListMIC - NegTokenInit2 + +% Same but with NegTokenInit2 + +srvssp = SPNEGOSSP([ + KerberosSSP(), + NTLMSSP(IDENTITIES={"User1": MD4le("Password123!")}) +]) +clissp = SPNEGOSSP([ + NTLMSSP(UPN="User1", PASSWORD="Password123!"), +]) + +srvctx, tok = srvssp.NegTokenInit2() +assert tok.MechType.val == '1.3.6.1.5.5.2' +assert [x.oid.val for x in tok.innerToken.token.mechTypes] == [ + '1.2.840.48018.1.2.2', + '1.2.840.113554.1.2.2', + '1.3.6.1.4.1.311.2.2.10', +] +assert tok.innerToken.token.reqFlags is None +assert tok.innerToken.token.mechToken is None +assert tok.innerToken.token.negHints.hintName.val == "not_defined_in_RFC4178@please_ignore" +assert tok.innerToken.token.mechListMIC is None +assert tok.innerToken.token._mechListMIC is None + +clictx, tok, status = clissp.GSS_Init_sec_context(None, tok) +assert clictx.require_mic +assert status == GSS_S_CONTINUE_NEEDED, status +assert len(tok.innerToken.token.mechTypes) == 1 +assert tok.innerToken.token.mechTypes[0].oid.val == '1.3.6.1.4.1.311.2.2.10' +assert tok.innerToken.token.mechListMIC is None +assert tok.innerToken.token._mechListMIC is None +assert isinstance(tok.innerToken.token.mechToken.value, NTLM_NEGOTIATE) + +srvctx, tok, status = srvssp.GSS_Accept_sec_context(srvctx, tok) +assert srvctx.require_mic +assert status == GSS_S_CONTINUE_NEEDED, status +assert tok.token.mechListMIC is None +assert tok.token.negState == 1 +assert tok.token.supportedMech.oid.val == '1.3.6.1.4.1.311.2.2.10' +assert isinstance(tok.token.responseToken.value, NTLM_CHALLENGE) + +clictx, tok, status = clissp.GSS_Init_sec_context(clictx, tok) +assert status == GSS_S_CONTINUE_NEEDED, status +assert tok.token.negState is None +assert tok.token.supportedMech is None +assert isinstance(tok.token.responseToken.value, NTLM_AUTHENTICATE) +assert isinstance(tok.token.mechListMIC.value, NTLMSSP_MESSAGE_SIGNATURE) +assert tok.token.mechListMIC.value.SeqNum == 0 +assert tok.token.mechListMIC.value.Version == 1 + +# INJECT FAULT: drop mechListMIC here, and make sure that the server doesn't let it go through. +tok.token.mechListMIC = None + +srvctx, tok, status = srvssp.GSS_Accept_sec_context(srvctx, tok) +assert status == GSS_S_CONTINUE_NEEDED, status # Should now be CONTINUE instead of COMPLETE ! + += SPNEGOSSP.from_cli_arguments - Utils + +from unittest import mock + +# Detect password prompts +def password_failure(*args, **kwargs): + raise ValueError("Password was prompted unexpectedly !") + +def password_input(*args, **kwargs): + return "Password" + + +def test_pwfail(**kwargs): + """Password means failure""" + with mock.patch('prompt_toolkit.prompt', side_effect=password_failure): + return SPNEGOSSP.from_cli_arguments(**kwargs) + + +def test_pwinput(**kwargs): + """Password is entered""" + with mock.patch('prompt_toolkit.prompt', side_effect=password_input): + return SPNEGOSSP.from_cli_arguments(**kwargs) + += SPNEGOSSP.from_cli_arguments - Username + Password - With input + +ssp = test_pwinput( + UPN="Administrator", + target="machine.domain.local", +) +assert isinstance(ssp, SPNEGOSSP) +assert len(ssp.ssps) == 1 +assert ssp.ssps[0].HASHNT == b'\xa4\xf4\x9c@e\x10\xbd\xca\xb6\x82N\xe7\xc3\x0f\xd8R' + += SPNEGOSSP.from_cli_arguments - Username + Password - With prompt + +try: + test_pwfail( + UPN="Administrator", + target="machine.domain.local", + ) + assert False, "Should have prompted for password !" +except ValueError: + pass + += SPNEGOSSP.from_cli_arguments - Username + Password - No input + +ssp = test_pwfail( + UPN="Administrator", + target="machine.domain.local", + password="Password", +) +assert isinstance(ssp, SPNEGOSSP) +assert len(ssp.ssps) == 1 +assert ssp.ssps[0].HASHNT == b'\xa4\xf4\x9c@e\x10\xbd\xca\xb6\x82N\xe7\xc3\x0f\xd8R' + += SPNEGOSSP.from_cli_arguments - UPN + Password - With input + +ssp = test_pwinput( + UPN="Administrator@domain.local", + target="machine.domain.local", +) +assert isinstance(ssp, SPNEGOSSP) +assert len(ssp.ssps) == 2 +assert isinstance(ssp.ssps[0], KerberosSSP) +assert ssp.ssps[0].UPN == "Administrator@domain.local" +assert isinstance(ssp.ssps[1], NTLMSSP) +assert ssp.ssps[1].HASHNT == b'\xa4\xf4\x9c@e\x10\xbd\xca\xb6\x82N\xe7\xc3\x0f\xd8R' + += SPNEGOSSP.from_cli_arguments - UPN + CCache - Prepare + +import os, base64 +from scapy.utils import get_temp_file + +# Create CCACHE +DATA = """ +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 +UJ8RjD9bppHbawAAAAA= +""" +ccache_file = get_temp_file() +with open(ccache_file, "wb") as fd: + fd.write(base64.b64decode(DATA.strip())) + +os.environ["KRB5CCNAME"] = ccache_file + += SPNEGOSSP.from_cli_arguments - UPN + CCache - TGT from KRB5CCNAME + +ssp = test_pwfail( + UPN="Administrator@domain.local", + target="machine.domain.local", + use_krb5ccname=True, +) +assert len(ssp.ssps) == 1 +assert ssp.ssps[0].TGT +assert not ssp.ssps[0].ST + += SPNEGOSSP.from_cli_arguments - UPN + CCache - TGT from ccache + +ssp = test_pwfail( + UPN="Administrator@domain.local", + target="machine.domain.local", + ccache=ccache_file +) +assert len(ssp.ssps) == 1 +assert ssp.ssps[0].TGT +assert not ssp.ssps[0].ST + += SPNEGOSSP.from_cli_arguments - UPN + CCache - ST from ccache + +ssp = test_pwfail( + UPN="Administrator@domain.local", + target="dc1.domain.local", + ccache=ccache_file +) +assert len(ssp.ssps) == 1 +assert ssp.ssps[0].ST +assert not ssp.ssps[0].TGT + += SPNEGOSSP.from_cli_arguments - UPN + CCache - Failure + +try: + test_pwfail( + UPN="Administrator@domain.local", + target="machine.domain.local", + ) + assert False, "Should have prompted for password !" +except ValueError: + pass + += SPNEGOSSP.from_cli_arguments - UPN + CCache - Bad UPN + +try: + test_pwfail( + UPN="toto@domain.local", + target="machine.domain.local", + ccache=ccache_file + ) + assert False, "Should have failed !" +except ValueError: + pass diff --git a/test/scapy/layers/tls/cert.uts b/test/scapy/layers/tls/cert.uts index f0a258e4db4..35a8d050059 100644 --- a/test/scapy/layers/tls/cert.uts +++ b/test/scapy/layers/tls/cert.uts @@ -332,8 +332,8 @@ assert abs(x.remainingDays("02/12/11")) > 5000 assert abs(x.remainingDays("Feb 12 10:00:00 2011 Paris, Madrid")) > 1 = Cert class : Checking RSA public key -assert type(x.pubKey) is PubKeyRSA -x_pubNum = x.pubKey.pubkey.public_numbers() +assert type(x.pubkey) is PubKeyRSA +x_pubNum = x.pubkey.pubkey.public_numbers() assert x_pubNum.n == 19231328316532061413420367242571475005688288081144416166988378525696075445024135424022026378563116068168327239354659928492979285632474448448624869172454076124150405352043642781483254546569202103296262513098482624188672299255268092629150366527784294463900039290024710152521604731213565912934889752122898104556895316819303096201441834849255370122572613047779766933573375974464479123135292080801384304131606933504677232323037116557327478512106367095125103346134248056463878553619525193565824925835325216545121044922690971718737998420984924512388011040969150550056783451476150234324593710633552558175109683813482739004163 x_pubNum.e == 0x10001 @@ -357,9 +357,9 @@ fstat = os.stat(filename) assert fstat.st_size == 1302 os.remove(filename) -= Cert class : isIssuerCert += Cert class : isIssuer -assert x.isIssuerCert(x) +assert x.isIssuer(x) = Cert class : Importing another PEM-encoded X.509 Certificate y = Cert(""" @@ -381,8 +381,8 @@ JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv """) = Cert class : Checking ECDSA public key -assert type(y.pubKey) is PubKeyECDSA -pubkey = y.pubKey.pubkey +assert type(y.pubkey) is PubKeyECDSA +pubkey = y.pubkey.pubkey assert pubkey.curve.name == 'secp384r1' pubkey.public_numbers().x == 3987178688175281746349180015490646948656137448666005327832107126183726641822596270780616285891030558662603987311874 @@ -524,7 +524,7 @@ with ContextManagerCaptureOutput() as cmco: ########### High-level methods ############################################### -= Cert class : Checking isIssuerCert() += Cert class : Checking isIssuer() c0 = Cert(""" -----BEGIN CERTIFICATE----- MIIFVjCCBD6gAwIBAgIJAJmDv7HOC+iUMA0GCSqGSIb3DQEBCwUAMIHGMQswCQYD @@ -614,130 +614,53 @@ pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 -----END CERTIFICATE----- """) -c0.isIssuerCert(c1) and c1.isIssuerCert(c2) and not c0.isIssuerCert(c2) +assert c0.isIssuer(c1) and c1.isIssuer(c2) and not c0.isIssuer(c2) = Cert class : Checking isSelfSigned() -c2.isSelfSigned() and not c1.isSelfSigned() and not c0.isSelfSigned() +assert c2.isSelfSigned() and not c1.isSelfSigned() and not c0.isSelfSigned() = PubKey class : Checking verifyCert() -c2.pubKey.verifyCert(c2) and c1.pubKey.verifyCert(c0) +assert c2.pubkey.verifyCert(c2) and c1.pubkey.verifyCert(c0) -= Chain class : Checking chain construction -assert len(Chain([c0, c1, c2])) == 3 -assert len(Chain([c0], c1)) == 2 -len(Chain([c0], c2)) == 1 += CertTree class : Checking verification of chain +chain0 = CertTree([c0, c1, c2]).getchain(c0) +assert len(chain0) == 3 +assert chain0[0] == c1 +assert chain0[1] == c0 +assert chain0[2] == c2 +chain1 = CertTree([c2, c1, c0]).getchain(c1) +assert len(chain1) == 2 +assert chain1[0] == c1 +assert chain1[1] == c2 +chain2 = CertTree([c0, c2, c1]).getchain(c2) +assert len(chain2) == 1 +assert chain2[0] == c2 -= Chain class : repr += CertTree class : show() -expected_repr = """__ /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Root Certificate Authority - G2 [Self Signed] - _ /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./OU=http://certs.starfieldtech.com/repository//CN=Starfield Secure Certificate Authority - G2 - _ /OU=Domain Control Validated/CN=*.tools.ietf.org""" -assert str(Chain([c0, c1, c2])) == expected_repr +expected_repr = '/C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./CN=Starfield Root Certificate Authority - G2 [Self Signed]\n /C=US/ST=Arizona/L=Scottsdale/O=Starfield Technologies, Inc./OU=http://certs.starfieldtech.com/repository//CN=Starfield Secure Certificate Authority - G2 [Not Self Signed]\n /OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n' +assert CertTree([c0, c1, c2]).show(ret=True) == expected_repr -= Chain class : Checking chain verification -assert Chain([], c0).verifyChain([c2], [c1]) -not Chain([c1]).verifyChain([c0]) +repr_str = CertTree([], c0).show(ret=True) +assert repr_str == '/OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n' -= Chain class: Checking chain verification with file += CertTree class : verify -import tempfile - -tf_folder = tempfile.mkdtemp() - -try: - os.makedirs(tf_folder) -except: - pass - -tf = os.path.join(tf_folder, "trusted") -utf = os.path.join(tf_folder, "untrusted") - -tf -utf - -# Create files -trusted = open(tf, "w") -trusted.write(""" ------BEGIN CERTIFICATE----- -MIIFADCCA+igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAw -MFoXDTMxMDUwMzA3MDAwMFowgcYxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTMwMQYDVQQLEypodHRwOi8vY2VydHMuc3RhcmZpZWxk -dGVjaC5jb20vcmVwb3NpdG9yeS8xNDAyBgNVBAMTK1N0YXJmaWVsZCBTZWN1cmUg -Q2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQDlkGZL7PlGcakgg77pbL9KyUhpgXVObST2yxcT+LBxWYR6ayuF -pDS1FuXLzOlBcCykLtb6Mn3hqN6UEKwxwcDYav9ZJ6t21vwLdGu4p64/xFT0tDFE -3ZNWjKRMXpuJyySDm+JXfbfYEh/JhW300YDxUJuHrtQLEAX7J7oobRfpDtZNuTlV -Bv8KJAV+L8YdcmzUiymMV33a2etmGtNPp99/UsQwxaXJDgLFU793OGgGJMNmyDd+ -MB5FcSM1/5DYKp2N57CSTTx/KgqT3M0WRmX3YISLdkuRJ3MUkuDq7o8W6o0OPnYX -v32JgIBEQ+ct4EMJddo26K3biTr1XRKOIwSDAgMBAAGjggEsMIIBKDAPBgNVHRMB -Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUJUWBaFAmOD07LSy+ -zWrZtj2zZmMwHwYDVR0jBBgwFoAUfAwyH6fZMH/EfWijYqihzqsHWycwOgYIKwYB -BQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5zdGFyZmllbGR0ZWNo -LmNvbS8wOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5zdGFyZmllbGR0ZWNo -LmNvbS9zZnJvb3QtZzIuY3JsMEwGA1UdIARFMEMwQQYEVR0gADA5MDcGCCsGAQUF -BwIBFitodHRwczovL2NlcnRzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkv -MA0GCSqGSIb3DQEBCwUAA4IBAQBWZcr+8z8KqJOLGMfeQ2kTNCC+Tl94qGuc22pN -QdvBE+zcMQAiXvcAngzgNGU0+bE6TkjIEoGIXFs+CFN69xpk37hQYcxTUUApS8L0 -rjpf5MqtJsxOYUPl/VemN3DOQyuwlMOS6eFfqhBJt2nk4NAfZKQrzR9voPiEJBjO -eT2pkb9UGBOJmVQRDVXFJgt5T1ocbvlj2xSApAer+rKluYjdkf5lO6Sjeb6JTeHQ -sPTIFwwKlhR8Cbds4cLYVdQYoKpBaXAko7nv6VrcPuuUSvC33l8Odvr7+2kDRUBQ -7nIMpBKGgc0T0U7EPMpODdIm8QC3tKai4W56gf0wrHofx1l7 ------END CERTIFICATE----- -""") -trusted.close() - -untrusted = open(utf, "w") -untrusted.write(""" ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx -EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT -HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs -ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw -MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj -aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp -Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg -nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 -HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N -Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN -dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 -HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G -CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU -sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 -4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg -8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K -pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 -mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE----- -""") -untrusted.close() - -assert Chain([], c0).verifyChainFromCAFile(tf, untrusted_file=utf) -assert Chain([], c0).verifyChainFromCAPath(tf_folder, untrusted_file=utf) - -= Clear files +CertTree([c1, c2]).verify(c0) +CertTree([c2]).verify(c1) try: - os.remove("./certs_test_ca/trusted") - os.remove("./certs_test_ca/untrusted") -except: + CertTree([c1]).verify(c0) + assert False +except ValueError: pass try: - os.rmdir("././certs_test_ca") -except: + CertTree([c2]).verify(c0) + assert False +except ValueError: pass -= Test __repr__ - -repr_str = Chain([], c0).__repr__() -assert repr_str == '__ /OU=Domain Control Validated/CN=*.tools.ietf.org [Not Self Signed]\n' = Test GeneralizedTime @@ -751,3 +674,434 @@ fd.write(b"-----END CERTIFICATE-----\n") fd.close() cert = Cert(filename) assert "2011" in cert.notBefore_str and "2046" in cert.notAfter_str + ++ CSR + += CSR class - Parse CSR in PKCS#10 format - Normal + +from scapy.layers.tls.cert import CSR + +csr = CSR(""" +-----BEGIN NEW CERTIFICATE REQUEST----- +MIIEEDCCAvgCAQAwIzEQMA4GA1UECgwHVGVzdE9yZzEPMA0GA1UEAwwGVGVzdENO +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7QfRs3mjYec2JJDiFJkA +Sq0hfCdWo9hbX75FfWAh2Pid4NsCu/8vsfHwezFoQehpzIJq7tXH6mip8fiIEtzA +3WsXq7AknJmoaSIGu/g85zoypgqUfKvvrsof6h8avg16unm/LiDe7GGUh135L61W +XjQRQZcb/J+e4NDe4vO1qKauviB8N29tarMgtWCN29xdP2MmWsKG7OdKoCkeCUBv +T8J6TwtOtgcnz88cg5TpNZd9DeloqQVO+Y6mB3F7Moup1RMzNNfvXX7VwJs19quH +QVvpGMqMdKvBOcFMWBzkp5yz8rz906Up6IMtJ5lebnZiM78qMASce09XXUoQeRa2 +yQIDAQABoIIBpjAcBgorBgEEAYI3DQIDMQ4WDDEwLjAuMjYxMDAuMjBCBgorBgEE +AYI3DQIBMTQwMh4mAEMAZQByAHQAaQBmAGkAYwBhAHQAZQBUAGUAbQBwAGwAYQB0 +AGUeCABVAHMAZQByMEcGCSsGAQQBgjcVFDE6MDgCAQkMEERDMS5ET01BSU4uTE9D +QUwMFERPTUFJTlxBZG1pbmlzdHJhdG9yDAtjZXJ0cmVxLmV4ZTB0BgorBgEEAYI3 +DQICMWYwZAIBAR5cAE0AaQBjAHIAbwBzAG8AZgB0ACAARQBuAGgAYQBuAGMAZQBk +ACAAQwByAHkAcAB0AG8AZwByAGEAcABoAGkAYwAgAFAAcgBvAHYAaQBkAGUAcgAg +AHYAMQAuADADAQAwgYIGCSqGSIb3DQEJDjF1MHMwFwYJKwYBBAGCNxQCBAoeCABV +AHMAZQByMCkGA1UdJQQiMCAGCisGAQQBgjcKAwQGCCsGAQUFBwMEBggrBgEFBQcD +AjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0OBBYEFHTYQp8vpznjtd/YUFReYLZ3KkGL +MA0GCSqGSIb3DQEBBQUAA4IBAQDB2DQqAlezF5kf5m7LQ46nacqmhi7UCKbAV4kV +REkBdOqLLac50rerzCbSLFD8py0dVtqzD8+ccsm/jmadIwIiGLy/4YLfFiSyA4CK +MiHpDb5iVCPlhA7uod9w0/tvtjDRvoipog1VA2B9GL2vDn2RAMa4BOnI2dM1OZdH +V7UC9kRspJrzO0wMl68VQU6t7cGi+jfxEO735OEfRGkRRUuBXq6azBswlelA4+Ha +997g/CwZmlIUB4D1vZrXZkIvLWybkGtlP369LePN91jUBJr2rDw4nyfulSZEDKwb +L8SaD6ZFk+ZchJPx0UhG13GHdUJZb2brHV+gYZRusX8LOauO +-----END NEW CERTIFICATE REQUEST----- +""") + +assert csr.verifySelf() +csr + +assert isinstance(csr.csr, PKCS10_CertificationRequest) +pkcs10 = csr.csr + +assert pkcs10.certificationRequestInfo.get_subject() == {'organizationName': 'TestOrg', 'commonName': 'TestCN'} + +assert pkcs10.certificationRequestInfo.attributes[0].type.oidname == 'OID_OS_VERSION' +assert pkcs10.certificationRequestInfo.attributes[0].values[0].value == b'10.0.26100.2' + +assert pkcs10.certificationRequestInfo.attributes[3].type.oidname == 'OID_ENROLLMENT_CSP_PROVIDER' +assert pkcs10.certificationRequestInfo.attributes[3].values[0].value.ProviderName.val.decode("utf-16be") == 'Microsoft Enhanced Cryptographic Provider v1.0' + +assert pkcs10.certificationRequestInfo.attributes[4].values[0].value.extensions[0].extnID.oidname == "ENROLL_CERTTYPE" +assert pkcs10.certificationRequestInfo.attributes[4].values[0].value.extensions[0].extnValue.Name.val == b'\x00U\x00s\x00e\x00r' + + += CSR class - Parse CSR in CMC format - Normal (sha1) + +from scapy.layers.tls.cert import CSR + +csr = CSR(""" +-----BEGIN NEW CERTIFICATE REQUEST----- +MIIGuwYJKoZIhvcNAQcCoIIGrDCCBqgCAQMxCzAJBgUrDgMCGgUAMIIFFQYIKwYB +BQUHDAKgggUHBIIFAzCCBP8wgdkwZgIBAgYKKwYBBAGCNwoKATFVMFMCAQAwAwIB +ATFJMEcGCSsGAQQBgjcVFDE6MDgCAQkMEERDMS5ET01BSU4uTE9DQUwMFERPTUFJ +TlxBZG1pbmlzdHJhdG9yDAtjZXJ0cmVxLmV4ZTBvAgEDBggrBgEFBQcHCDFgMF4C +AQAwAwIBATBUMBcGCSsGAQQBgjcUAgQKHggAVQBzAGUAcjApBgNVHSUEIjAgBgor +BgEEAYI3CgMEBggrBgEFBQcDBAYIKwYBBQUHAwIwDgYDVR0PAQH/BAQDAgWgMIIE +G6CCBBcCAQEwggQQMIIC+AIBADAjMRAwDgYDVQQKDAdUZXN0T3JnMQ8wDQYDVQQD +DAZUZXN0Q04wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDDmuBEjKaX +ETewhcD5qoa4Qx+6H7YWColwCukmf6Bg8lwq3nHXEXHZjBfZaLHcev1xEgKd25L9 +kZtMUYeMK/zxX2C0x5xbsJR18/sJ9GZ5YqzUhlGW8PaLaql6g2e+jpzd9m1kU+IK +3Wx/bwZ2TmMjZoikHPym4Kzieyimqty8FxU1fBNkcNopLxaFVlclprwXqrdHXALC +/OWZIzUNcjAaTnVU4qFx7Mb65ik4klVsRiO1nR1dVeJWbMa8gvUhcglsZyH/wRwD +lHwpYGY1LO27Z9baBJojraTr0z0YiOWj6BjWfRy9FsbI+sFwLzskaUbcjdvI+UmL +P7Nji0zGrksZAgMBAAGgggGmMBwGCisGAQQBgjcNAgMxDhYMMTAuMC4yNjEwMC4y +MEIGCisGAQQBgjcNAgExNDAyHiYAQwBlAHIAdABpAGYAaQBjAGEAdABlAFQAZQBt +AHAAbABhAHQAZR4IAFUAcwBlAHIwRwYJKwYBBAGCNxUUMTowOAIBCQwQREMxLkRP +TUFJTi5MT0NBTAwURE9NQUlOXEFkbWluaXN0cmF0b3IMC2NlcnRyZXEuZXhlMHQG +CisGAQQBgjcNAgIxZjBkAgEBHlwATQBpAGMAcgBvAHMAbwBmAHQAIABFAG4AaABh +AG4AYwBlAGQAIABDAHIAeQBwAHQAbwBnAHIAYQBwAGgAaQBjACAAUAByAG8AdgBp +AGQAZQByACAAdgAxAC4AMAMBADCBggYJKoZIhvcNAQkOMXUwczAXBgkrBgEEAYI3 +FAIECh4IAFUAcwBlAHIwKQYDVR0lBCIwIAYKKwYBBAGCNwoDBAYIKwYBBQUHAwQG +CCsGAQUFBwMCMA4GA1UdDwEB/wQEAwIFoDAdBgNVHQ4EFgQUJm+5vprbywwUFsNc +npQt/zP+3BEwDQYJKoZIhvcNAQEFBQADggEBAHbi+2oaM9oRugYC6lfmRJHOKJ62 +DP/A41KZEe+7tZfrLSeOra6hNg10LZCJXE3Sg8rzE26/ZU/raJjxNytHH4NuhyxV +gZddYbekxRMey3ou2qSbotRATWYVEt7eW3+eJxunpEiuAZ4+Q0l5OcoT2XY85m+V +0goQFs3VHDDhbiDdm2TFikNA6Soi0H3Fe9Fdy36N9ua7Z5EwPhNCkorVU4C+XA+u +qJu2P18+W2p0NjQz96QfmBB0QXc2b0bCRpcsuQG9T3h0S1nrWXKjoymJL4SZPZ3F +Za/zNAPTPHd4UlX3fC5vPV1tw/sGfqn5ICRiqbNFIRPixol0UJmP/t0IMoowADAA +MYIBezCCAXcCAQOAFCZvub6a28sMFBbDXJ6ULf8z/twRMAkGBSsOAwIaBQCgPjAX +BgkqhkiG9w0BCQMxCgYIKwYBBQUHDAIwIwYJKoZIhvcNAQkEMRYEFCYLXDzJ+xwT +oNgEnqjniydkGdJkMA0GCSqGSIb3DQEBAQUABIIBAHgMd4MCpLOlR+Z12ATKYfgc +EeA/npahMiXIC97vn24xRDz9gXvQAvw9mSrhwc3kHPF45fQ4eQcoyaYHIt2G93sf +5rF8g9qKsNkzqvpVHPD1CUImUBxW92n1NwYwdL711x22wehaabWxybS5L3BLdLqr +B86oxjQ582eGXqj4OncPesPBJud4AnD4ObZseDFk3sNfJmiz3e7BUkOhcfPgePZv +P4xCVfb5QBV4Jv6xem/0QxkpuQNyTrEK7g3qE0DZaOj97YEPOtqKeOcSddueTPjy +u7F+oKWjUdO6P903GzjyeI5D3pV3WPgkz2a2lzdB2+B1Zw4MyXvUpfeg9CkkbCs= +-----END NEW CERTIFICATE REQUEST----- +""") + +assert csr.verifySelf() + +assert isinstance(csr.csr, CMS_ContentInfo) +cms = csr.csr + +assert cms.content.signerInfos[0].version == 3 +assert cms.content.signerInfos[0].sid.sid == b'&o\xb9\xbe\x9a\xdb\xcb\x0c\x14\x16\xc3\\\x9e\x94-\xff3\xfe\xdc\x11' +assert cms.content.signerInfos[0].digestAlgorithm.algorithm.oidname == "sha1" +assert cms.content.signerInfos[0].signatureAlgorithm.algorithm.oidname == "rsaEncryption" + += CSR class - Parse CSR in CMC format - Normal (sha1) - Unpack and verify + +cms_engine = CMS_Engine([csr]) +pkidata = cms_engine.verify(cms) + +assert isinstance(pkidata, CMC_PKIData) + +assert len(pkidata.controlSequence) == 2 +assert pkidata.controlSequence[0].type.oidname == "OID_CMC_ADD_ATTRIBUTES" +assert pkidata.controlSequence[0].attrValues[0].value.pkiDataReference == 0 +assert pkidata.controlSequence[0].attrValues[0].value.certReferences == [1] +assert pkidata.controlSequence[0].attrValues[0].value.attributes[0].values[0].value.MachineName == b"DC1.DOMAIN.LOCAL" +assert pkidata.controlSequence[0].attrValues[0].value.attributes[0].values[0].value.UserName == b"DOMAIN\\Administrator" +assert pkidata.controlSequence[0].attrValues[0].value.attributes[0].values[0].value.ProcessName == b"certreq.exe" + +assert pkidata.controlSequence[1].type.oidname == "id-cmc-addExtensions" +assert isinstance(pkidata.controlSequence[1].attrValues[0].value.extensions[0].extensions[0].extnValue, X509_ExtCertificateTemplateName) +assert pkidata.controlSequence[1].attrValues[0].value.extensions[0].extensions[0].extnValue.Name == b'\x00U\x00s\x00e\x00r' +assert isinstance(pkidata.controlSequence[1].attrValues[0].value.extensions[0].extensions[1].extnValue, X509_ExtExtendedKeyUsage) +assert [x.oid.oidname for x in pkidata.controlSequence[1].attrValues[0].value.extensions[0].extensions[1].extnValue.extendedKeyUsage] == ['OID_EFS_CRYPTO', 'emailProtection', 'clientAuth'] +assert isinstance(pkidata.controlSequence[1].attrValues[0].value.extensions[0].extensions[2].extnValue, X509_ExtKeyUsage) +assert pkidata.controlSequence[1].attrValues[0].value.extensions[0].extensions[2].extnValue.keyUsage == "101" + +assert isinstance(pkidata.reqSequence[0], CMC_TaggedRequest) +certReqInfo = pkidata.reqSequence[0].request.certificationRequest.certificationRequestInfo +assert certReqInfo.version == 0 +assert certReqInfo.get_subject() == {'organizationName': 'TestOrg', 'commonName': 'TestCN'} +assert certReqInfo.get_subject_str() == '/O=TestOrg/CN=TestCN' + +assert certReqInfo.attributes[0].type.oidname == "OID_OS_VERSION" +assert certReqInfo.attributes[0].values[0].value == b'10.0.26100.2' + +assert certReqInfo.attributes[1].type.oidname == "OID_ENROLLMENT_NAME_VALUE_PAIR" +assert certReqInfo.attributes[1].values[0].value.Name.val.decode("utf-16be") == "CertificateTemplate" +assert certReqInfo.attributes[1].values[0].value.Value.val.decode("utf-16be") == "User" + +assert certReqInfo.attributes[2].values[0].value.MachineName == b'DC1.DOMAIN.LOCAL' +assert certReqInfo.attributes[2].values[0].value.UserName == b'DOMAIN\\Administrator' +assert certReqInfo.attributes[2].values[0].value.ProcessName == b'certreq.exe' + +assert certReqInfo.attributes[3].values[0].value.KeySpec == 1 +assert certReqInfo.attributes[3].values[0].value.ProviderName.val.decode("utf-16be") == 'Microsoft Enhanced Cryptographic Provider v1.0' + +assert certReqInfo.attributes[4].values[0].value.extensions[0].extnValue.Name == b"\x00U\x00s\x00e\x00r" +assert [x.oid.oidname for x in certReqInfo.attributes[4].values[0].value.extensions[1].extnValue.extendedKeyUsage] == ['OID_EFS_CRYPTO', 'emailProtection', 'clientAuth'] +assert isinstance(certReqInfo.attributes[4].values[0].value.extensions[3].extnValue, X509_ExtSubjectKeyIdentifier) +assert certReqInfo.attributes[4].values[0].value.extensions[3].extnValue.keyIdentifier == b'&o\xb9\xbe\x9a\xdb\xcb\x0c\x14\x16\xc3\\\x9e\x94-\xff3\xfe\xdc\x11' + += CSR class - Parse CSR in CMC format - Advanced (KeyAttestation + sha256) + +from scapy.layers.tls.cert import CSR + +csr = CSR(""" + -----BEGIN NEW CERTIFICATE REQUEST----- + MIIdrAYJKoZIhvcNAQcCoIIdnTCCHZkCAQMxDzANBglghkgBZQMEAgEFADCCG/IG + CCsGAQUFBwwCoIIb5ASCG+AwghvcMGUwYwIBAgYKKwYBBAGCNwoKATFSMFACAQAw + AwIBATFGMEQGCSsGAQQBgjcVFDE3MDUCAQUMEldLUzAxLkRPTUFJTi5MT0NBTAwN + RE9NQUlOXFdLUzAxJAwNdGFza2hvc3R3LmV4ZTCCG22gghtpAgEBMIIbYjCCGkoC + AQAwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALBtO+wrGSWT7QVn + 6dfFxKs/Yy2PRXZTzfd6770wcCZNPT/NzaN2QqVVwm1LMLTwgiHJQR2iUW8mfYLu + uVWRBvNi5DVAaB7LwNQxfb1XjaoDz5mmopISJPwjSZWkBlTj3V243f78GnR4kxXj + Df8Aw8Soggi2ijlvYuBtmDK/lwLhxg9Q0o477yBsjZRWp0VrB/QFL612ajN7t3EF + sLwSovMws+a1dAJQnzQozCmTFUiiAJCfulvqqXo73Lf9piYldIR2PQ93vNLK7PEv + 1h6PkiBQnkHEp2WxY0fCx8+hr44o2+fnZ789zKtVU8Hl7tVn9l+m80nfX69rYZDO + XnvNd2cCAwEAAaCCGRswHAYKKwYBBAGCNw0CAzEOFgwxMC4wLjI2MjAwLjIwRAYJ + KwYBBAGCNxUUMTcwNQIBBQwSV0tTMDEuRE9NQUlOLkxPQ0FMDA1ET01BSU5cV0tT + MDEkDA10YXNraG9zdHcuZXhlMFMGCSsGAQQBgjcVGTFGHkQATQBpAGMAcgBvAHMA + bwBmAHQAIABQAGwAYQB0AGYAbwByAG0AIABDAHIAeQBwAHQAbwAgAFAAcgBvAHYA + aQBkAGUAcjBcBgorBgEEAYI3DQICMU4wTAIBAB5EAE0AaQBjAHIAbwBzAG8AZgB0 + ACAAUABsAGEAdABmAG8AcgBtACAAQwByAHkAcAB0AG8AIABQAHIAbwB2AGkAZABl + AHIDAQAwgbAGCSqGSIb3DQEJDjGBojCBnzA8BgkrBgEEAYI3FQcELzAtBiUrBgEE + AYI3FQjWsQSC2qUvhJmLPoL2iwyCzYNAa4bp+RyCq/F7AgFkAgEHMBMGA1UdJQQM + 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 + tDcJu1pwwhhwrsR9l8iKGw== + -----END NEW CERTIFICATE REQUEST----- +""".replace(" ", "")) + +assert csr.verifySelf() + +assert isinstance(csr.csr, CMS_ContentInfo) +cms = csr.csr + +assert cms.content.signerInfos[0].version == 3 +assert cms.content.signerInfos[0].sid.sid == b'\x17\xad/(\x94]\x19e]\x8c\xaa\xda~\x85-\xd6i\x11\xef\xb5' +assert cms.content.signerInfos[0].digestAlgorithm.algorithm.oidname == "sha256" +assert cms.content.signerInfos[0].signatureAlgorithm.algorithm.oidname == "rsaEncryption" + += CSR class - Parse CSR in CMC format - Advanced (KeyAttestation + sha256) - Unpack and verify + +from scapy.layers.tpm import * + +cms_engine = CMS_Engine([csr]) +pkidata = cms_engine.verify(cms) + +assert isinstance(pkidata, CMC_PKIData) + +assert isinstance(pkidata.reqSequence[0], CMC_TaggedRequest) +certReqInfo = pkidata.reqSequence[0].request.certificationRequest.certificationRequestInfo +assert certReqInfo.version == 0 +assert certReqInfo.attributes[0].type.oidname == "OID_OS_VERSION" +assert certReqInfo.attributes[0].values[0].value == b'10.0.26200.2' + +assert certReqInfo.attributes[2].type.oidname == 'OID_ENROLL_KSP_NAME' +assert certReqInfo.attributes[2].values[0].value.val.decode("utf-16be") == 'Microsoft Platform Crypto Provider' + +assert certReqInfo.attributes[3].type.oidname == "OID_ENROLLMENT_CSP_PROVIDER" +assert certReqInfo.attributes[3].values[0].value.ProviderName.val.decode("utf-16be") == 'Microsoft Platform Crypto Provider' + +assert certReqInfo.attributes[4].type.oidname == "extensionRequest" +exts = certReqInfo.attributes[4].values[0].value.extensions + +assert exts[0].extnID.oidname == "OID_CERTIFICATE_TEMPLATE" +assert exts[0].extnValue.templateID.val == '1.3.6.1.4.1.311.21.8.1415300.5673647.8799678.6129036.5456320.107.14318748.4913403' +assert exts[0].extnValue.templateMajorVersion == 100 +assert exts[0].extnValue.templateMinorVersion == 7 + +assert exts[1].extnID.oidname == "extKeyUsage" +assert exts[1].extnValue.extendedKeyUsage[0].oid.oidname == 'iKEIntermediate' + +assert exts[2].extnID.oidname == "keyUsage" +assert bool(exts[2].critical) is True +assert exts[2].extnValue.keyUsage == "101" + +assert exts[3].extnID.oidname == "OID_APPLICATION_CERT_POLICIES" +assert isinstance(exts[3].extnValue, X509_ExtCertificatePolicies) +assert exts[3].extnValue.certificatePolicies[0].policyIdentifier.oidname == 'iKEIntermediate' + +assert exts[4].extnID.oidname == "subjectKeyIdentifier" +assert exts[4].extnValue.keyIdentifier == b"\x17\xad/(\x94]\x19e]\x8c\xaa\xda~\x85-\xd6i\x11\xef\xb5" + +assert certReqInfo.attributes[5].type.oidname == "OID_ENROLL_EK_INFO" +cms_enveloppe = certReqInfo.attributes[5].values[0].value +assert cms_enveloppe.contentType.oidname == "id-envelopedData" +assert isinstance(cms_enveloppe.content.recipientInfos[0].recipientInfo, CMS_KeyTransRecipientInfo) +assert cms_enveloppe.content.encryptedContentInfo.contentType.oidname == 'pkcs-7.1' + +assert certReqInfo.attributes[6].type.oidname == "OID_ENROLL_ATTESTATION_STATEMENT" +cmc_enrollment = certReqInfo.attributes[6].values[0].value +assert isinstance(cmc_enrollment.kas, KeyAttestationStatement) +assert cmc_enrollment.kas.Version == 1 + +idBinding = cmc_enrollment.kas.idBinding +assert isinstance(idBinding, PCP_IDBinding20) +assert idBinding.PublicKey.publicArea.objectAttributes == 328818 +assert idBinding.PublicKey.publicArea.authPolicy.buffer == b'\x9d\xff\xcb\xf3l8:\xe6\x99\xfb\x98h\xdcm\xcb\x89\xd7\x158\x84\xbe(\x03\x92,\x12AX\xbf\xad"\xae' +assert idBinding.CreationData.creationData.pcrSelect.count == 0 +assert idBinding.CreationData.creationData.pcrDigest.buffer == b"\xe3\xb0\xc4B\x98\xfc\x1c\x14\x9a\xfb\xf4\xc8\x99o\xb9$'\xaeA\xe4d\x9b\x93L\xa4\x95\x99\x1bxR\xb8U" +assert idBinding.CreationData.creationData.locality.Extended == 1 +assert idBinding.CreationData.creationData.sprintf("%parentNameAlg%") == 'TPM_ALG_SHA256' +assert idBinding.CreationData.creationData.parentName.Name == b'\x00\x0b\x0e\xa7[\x12x5\xe3_\x10\xc7\xfc\xcd3Za\xe3.\xde\xf6\xbc\x0b\xd9Y\xbamn\x87I\xb6b\x8b\xa8' +assert idBinding.Attest.attestationData.qualifiedSigned.Name == b'\x00\x0b\xb5\xb4e\x88\xdbDN\x9c\xf0ICV\xd7\xc5]n\x91\r|\xd5\x89D\x9a]\xebL\x04\x90\xbb$\xd7\x0f' +assert idBinding.Attest.attestationData.extraData.buffer == b'w\x06\xa2\x0f*\xef2\xd8\x00C\x85X$\n\xed\x1c\xectlT' +assert idBinding.Attest.attestationData.attested.objectName.Name == b"\x00\x0b'\x82s(\n\x07\x19\x99Wo\\\xac\xe3ZMiUr\xe9\x0fZ2\x8b\xb5{\xb1C\xff-\xccz\xa3" +assert idBinding.Attest.attestationData.attested.creationHash.buffer == b'.\xd0\xb6\ns\xf1\x0b\x8a\xdb\x08Z\x0c\xfe(\xec\xe3\xf6C\x1dp\xf3\xe6\x07\x9c879\x83~\x824f' +assert idBinding.Signature.sprintf("%sigAlg%") == "TPM_ALG_RSASSA" +assert idBinding.Signature.signature.sprintf("%hash%") == "TPM_ALG_SHA1" + +keyAttestation = cmc_enrollment.kas.keyAttestation +assert isinstance(keyAttestation, KeyAttestation) +assert keyAttestation.sprintf("%Platform%") == "TPM 2.0" +assert keyAttestation.keyBlob.flags & 2 +assert keyAttestation.keyBlob.public.publicArea.objectAttributes.fixedTPM +assert keyAttestation.keyBlob.public.publicArea.objectAttributes.userWithAuth +assert keyAttestation.keyBlob.public.publicArea.parameters.symmetric.sprintf("%algorithm%") == 'TPM_ALG_NULL' +assert keyAttestation.keyBlob.public.publicArea.parameters.scheme.sprintf("%scheme%") == 'TPM_ALG_NULL' +assert [x.buffer for x in keyAttestation.keyBlob.policyDigestList.digests] == [ + b'\x8f\xcd!i\xab\x92iN\x0cc?\x1a\xb7r\x84+\x82A\xbb\xc2\x02\x88\x98\x1f\xc7\xac\x1e\xdd\xc1\xfd\xdb\x0e', + b'\xe5)\xf5\xd6\x11(r\x95N\x8e\xd6`Q\x17\xb7W\xe27\xc6\xe1\x95\x13\xa9I\xfe\xe1\xf2\x04\xc4X\x02:', + b'\xaf,\xa5ii\x9cCj!\x00o\x1c\xb8\xa2ul\x98\xbc\x1cvZ5Y\xc5\xfe\x1c?^r(\xa7\xe7', + b'\xc4\x13\xa8G\xb1\x11\x12\xb1\xcb\xdd\xd4\xec\xa4\xda\xaa\x15\xa1\x85,\x1c;\xbaWF\x1d%v\x05\xf3\xd5\xafS', + b'', + b'\x04\x8e\x9a:\xce\x08X?y\xf3D\xffx[\xbe\xa9\xf0z\xc7\xfa3%\xb3\xd4\x9a!\xddQ\x94\xc6XP', +] + +aikOpaque = cmc_enrollment.kas.aikOpaque +assert isinstance(aikOpaque, PCP_20_KEY_BLOB) +assert aikOpaque.public.publicArea.unique.buffer == b'\xc6\x1c\x7f\xc4KC\xc2 \x9b;a/\x888\x84\x0e4w5\xb4\x01\'\xb8/\x1c\xc2\xe9\xc3;v\x8dUb4\x8c\xde\xd09\xd9\x1dSPC\xaa\xcb\xcc[N6Z\xedK\\\xa8\xe3\x94h:\x04(|\xc1\x08\x15\x12]Z\xf7\xc6,\x94\x9c\xe1\x145e\xee\x8c\x89/\xd0\xea>\x91\xc1\xb4\x1cl\xc5\xaa\x9c\xca)\t\xbb\nhv\xdb3\xf3\xbe\xdcLYj\xc8g.VN\xdc\xbd\x95\xfc\x05"\xc7\xc7\xf2{w\x1b\x18\xba\xf6\xa4\x93\x00V?\x87 \xa5U\x96\x807\xa5\xa5\x0b\x11O\x7f\x1br\x96H\x1e1\xf7z\xcc\xd7\x1e\x132SD\xc1F\x9d|Ud9]\xa3[\xff6\xa9\xc0\xa7\x9e]\xf4\xdc=1\xe7\xa9i\xc9\xcaI\xd9\x80\x84\xa3\xe7f\x0f\n\xb2\xe9o\x13\x12Vg}\x85\xbd\x14^<\xe6\x10WQ3\x0e\xcci\x99(\xc60\x87\xa4+\xbdw\x03RD\xef\x87/[\x9f\x04C}\xf2\x18]\x8f\xb4x\x04\xb2\xe6\xd8h\xf9\xad\xef\x88?\xea\x88\x07\xecu' +assert [x.buffer for x in aikOpaque.policyDigestList.digests] == [ + b'\x8f\xcd!i\xab\x92iN\x0cc?\x1a\xb7r\x84+\x82A\xbb\xc2\x02\x88\x98\x1f\xc7\xac\x1e\xdd\xc1\xfd\xdb\x0e', + b'\xe5)\xf5\xd6\x11(r\x95N\x8e\xd6`Q\x17\xb7W\xe27\xc6\xe1\x95\x13\xa9I\xfe\xe1\xf2\x04\xc4X\x02:', + b'\xaf,\xa5ii\x9cCj!\x00o\x1c\xb8\xa2ul\x98\xbc\x1cvZ5Y\xc5\xfe\x1c?^r(\xa7\xe7', + b'\xc4\x13\xa8G\xb1\x11\x12\xb1\xcb\xdd\xd4\xec\xa4\xda\xaa\x15\xa1\x85,\x1c;\xbaWF\x1d%v\x05\xf3\xd5\xafS', + b'', + b'\x04\x8e\x9a:\xce\x08X?y\xf3D\xffx[\xbe\xa9\xf0z\xc7\xfa3%\xb3\xd4\x9a!\xddQ\x94\xc6XP', +] \ No newline at end of file