mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-28 16:28:33 +03:00
smdpp: less verbose by default
Those data blobs are huge. Change-Id: I04a72b8f52417862d4dcba1f0743700dd942ef49
This commit is contained in:
@@ -47,6 +47,9 @@ from pySim.esim.es8p import *
|
|||||||
from pySim.esim.x509_cert import oid, cert_policy_has_oid, cert_get_auth_key_id
|
from pySim.esim.x509_cert import oid, cert_policy_has_oid, cert_get_auth_key_id
|
||||||
from pySim.esim.x509_cert import CertAndPrivkey, CertificateSet, cert_get_subject_key_id, VerifyError
|
from pySim.esim.x509_cert import CertAndPrivkey, CertificateSet, cert_get_subject_key_id, VerifyError
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# HACK: make this configurable
|
# HACK: make this configurable
|
||||||
DATA_DIR = './smdpp-data'
|
DATA_DIR = './smdpp-data'
|
||||||
HOSTNAME = 'testsmdpplus1.example.com' # must match certificates!
|
HOSTNAME = 'testsmdpplus1.example.com' # must match certificates!
|
||||||
@@ -106,12 +109,12 @@ def parse_permitted_eins_from_cert(eum_cert) -> List[str]:
|
|||||||
|
|
||||||
if cert_variant == 'O':
|
if cert_variant == 'O':
|
||||||
# Old variant - use nameConstraints extension
|
# Old variant - use nameConstraints extension
|
||||||
print("Using nameConstraints parsing for variant O certificate")
|
#print("Using nameConstraints parsing for variant O certificate")
|
||||||
permitted_iins.extend(_parse_name_constraints_eins(eum_cert))
|
permitted_iins.extend(_parse_name_constraints_eins(eum_cert))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# New variants (Ov3, A, B, C) - use GSMA permittedEins extension
|
# New variants (Ov3, A, B, C) - use GSMA permittedEins extension
|
||||||
print("Using GSMA permittedEins parsing for newer certificate variant")
|
#print("Using GSMA permittedEins parsing for newer certificate variant")
|
||||||
permitted_iins.extend(_parse_gsma_permitted_eins(eum_cert))
|
permitted_iins.extend(_parse_gsma_permitted_eins(eum_cert))
|
||||||
|
|
||||||
unique_iins = list(set(permitted_iins))
|
unique_iins = list(set(permitted_iins))
|
||||||
@@ -178,13 +181,13 @@ def _parse_name_constraints_eins(eum_cert) -> List[str]:
|
|||||||
x509.oid.ExtensionOID.NAME_CONSTRAINTS
|
x509.oid.ExtensionOID.NAME_CONSTRAINTS
|
||||||
)
|
)
|
||||||
|
|
||||||
print("Found nameConstraints extension (variant O)")
|
# print("Found nameConstraints extension (variant O)")
|
||||||
name_constraints = name_constraints_ext.value
|
name_constraints = name_constraints_ext.value
|
||||||
|
|
||||||
# Check permittedSubtrees for IIN constraints
|
# Check permittedSubtrees for IIN constraints
|
||||||
if name_constraints.permitted_subtrees:
|
if name_constraints.permitted_subtrees:
|
||||||
for subtree in name_constraints.permitted_subtrees:
|
for subtree in name_constraints.permitted_subtrees:
|
||||||
print(f"Processing permitted subtree: {subtree}")
|
# print(f"Processing permitted subtree: {subtree}")
|
||||||
|
|
||||||
if isinstance(subtree, x509.DirectoryName):
|
if isinstance(subtree, x509.DirectoryName):
|
||||||
for attribute in subtree.value:
|
for attribute in subtree.value:
|
||||||
@@ -299,11 +302,11 @@ class SmDppHttpServer:
|
|||||||
def ci_get_cert_for_pkid(self, ci_pkid: bytes) -> Optional[x509.Certificate]:
|
def ci_get_cert_for_pkid(self, ci_pkid: bytes) -> Optional[x509.Certificate]:
|
||||||
"""Find CI certificate for given key identifier."""
|
"""Find CI certificate for given key identifier."""
|
||||||
for cert in self.ci_certs:
|
for cert in self.ci_certs:
|
||||||
print("cert: %s" % cert)
|
logger.debug("cert: %s" % cert)
|
||||||
subject_exts = list(filter(lambda x: isinstance(x.value, x509.SubjectKeyIdentifier), cert.extensions))
|
subject_exts = list(filter(lambda x: isinstance(x.value, x509.SubjectKeyIdentifier), cert.extensions))
|
||||||
print(subject_exts)
|
logger.debug(subject_exts)
|
||||||
subject_pkid = subject_exts[0].value
|
subject_pkid = subject_exts[0].value
|
||||||
print(subject_pkid)
|
logger.debug(subject_pkid)
|
||||||
if subject_pkid and subject_pkid.key_identifier == ci_pkid:
|
if subject_pkid and subject_pkid.key_identifier == ci_pkid:
|
||||||
return cert
|
return cert
|
||||||
return None
|
return None
|
||||||
@@ -356,7 +359,7 @@ class SmDppHttpServer:
|
|||||||
validate_request_headers(request)
|
validate_request_headers(request)
|
||||||
|
|
||||||
content = json.loads(request.content.read())
|
content = json.loads(request.content.read())
|
||||||
print("Rx JSON: %s" % json.dumps(content))
|
#logger.debug("Rx JSON: %s" % json.dumps(content))
|
||||||
set_headers(request)
|
set_headers(request)
|
||||||
|
|
||||||
output = func(self, request, content)
|
output = func(self, request, content)
|
||||||
@@ -364,7 +367,7 @@ class SmDppHttpServer:
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
build_resp_header(output)
|
build_resp_header(output)
|
||||||
print("Tx JSON: %s" % json.dumps(output))
|
logger.debug("Tx JSON: %s" % json.dumps(output)[:200])
|
||||||
return json.dumps(output)
|
return json.dumps(output)
|
||||||
return _api_wrapper
|
return _api_wrapper
|
||||||
|
|
||||||
@@ -383,7 +386,7 @@ class SmDppHttpServer:
|
|||||||
|
|
||||||
euiccInfo1_bin = b64decode(content['euiccInfo1'])
|
euiccInfo1_bin = b64decode(content['euiccInfo1'])
|
||||||
euiccInfo1 = rsp.asn1.decode('EUICCInfo1', euiccInfo1_bin)
|
euiccInfo1 = rsp.asn1.decode('EUICCInfo1', euiccInfo1_bin)
|
||||||
print("Rx euiccInfo1: %s" % euiccInfo1)
|
logger.debug("Rx euiccInfo1: %s" % euiccInfo1)
|
||||||
#euiccInfo1['svn']
|
#euiccInfo1['svn']
|
||||||
|
|
||||||
# TODO: If euiccCiPKIdListForSigningV3 is present ...
|
# TODO: If euiccCiPKIdListForSigningV3 is present ...
|
||||||
@@ -458,7 +461,7 @@ class SmDppHttpServer:
|
|||||||
|
|
||||||
authenticateServerResp_bin = b64decode(content['authenticateServerResponse'])
|
authenticateServerResp_bin = b64decode(content['authenticateServerResponse'])
|
||||||
authenticateServerResp = rsp.asn1.decode('AuthenticateServerResponse', authenticateServerResp_bin)
|
authenticateServerResp = rsp.asn1.decode('AuthenticateServerResponse', authenticateServerResp_bin)
|
||||||
print("Rx %s: %s" % authenticateServerResp)
|
logger.debug("Rx %s: %s" % authenticateServerResp)
|
||||||
if authenticateServerResp[0] == 'authenticateResponseError':
|
if authenticateServerResp[0] == 'authenticateResponseError':
|
||||||
r_err = authenticateServerResp[1]
|
r_err = authenticateServerResp[1]
|
||||||
#r_err['transactionId']
|
#r_err['transactionId']
|
||||||
@@ -589,7 +592,7 @@ class SmDppHttpServer:
|
|||||||
|
|
||||||
prepDownloadResp_bin = b64decode(content['prepareDownloadResponse'])
|
prepDownloadResp_bin = b64decode(content['prepareDownloadResponse'])
|
||||||
prepDownloadResp = rsp.asn1.decode('PrepareDownloadResponse', prepDownloadResp_bin)
|
prepDownloadResp = rsp.asn1.decode('PrepareDownloadResponse', prepDownloadResp_bin)
|
||||||
print("Rx %s: %s" % prepDownloadResp)
|
logger.debug("Rx %s: %s" % prepDownloadResp)
|
||||||
|
|
||||||
if prepDownloadResp[0] == 'downloadResponseError':
|
if prepDownloadResp[0] == 'downloadResponseError':
|
||||||
r_err = prepDownloadResp[1]
|
r_err = prepDownloadResp[1]
|
||||||
@@ -611,7 +614,7 @@ class SmDppHttpServer:
|
|||||||
|
|
||||||
# store otPK.EUICC.ECKA in session state
|
# store otPK.EUICC.ECKA in session state
|
||||||
ss.euicc_otpk = euiccSigned2['euiccOtpk']
|
ss.euicc_otpk = euiccSigned2['euiccOtpk']
|
||||||
print("euiccOtpk: %s" % (b2h(ss.euicc_otpk)))
|
logger.debug("euiccOtpk: %s" % (b2h(ss.euicc_otpk)))
|
||||||
|
|
||||||
# Generate a one-time ECKA key pair (ot{PK,SK}.DP.ECKA) using the curve indicated by the Key Parameter
|
# Generate a one-time ECKA key pair (ot{PK,SK}.DP.ECKA) using the curve indicated by the Key Parameter
|
||||||
# Reference value of CERT.DPpb.ECDDSA
|
# Reference value of CERT.DPpb.ECDDSA
|
||||||
@@ -619,8 +622,8 @@ class SmDppHttpServer:
|
|||||||
ss.smdp_ot = ec.generate_private_key(self.dp_pb.get_curve())
|
ss.smdp_ot = ec.generate_private_key(self.dp_pb.get_curve())
|
||||||
# extract the public key in (hopefully) the right format for the ES8+ interface
|
# extract the public key in (hopefully) the right format for the ES8+ interface
|
||||||
ss.smdp_otpk = ss.smdp_ot.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
|
ss.smdp_otpk = ss.smdp_ot.public_key().public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
|
||||||
print("smdpOtpk: %s" % b2h(ss.smdp_otpk))
|
logger.debug("smdpOtpk: %s" % b2h(ss.smdp_otpk))
|
||||||
print("smdpOtsk: %s" % b2h(ss.smdp_ot.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption())))
|
logger.debug("smdpOtsk: %s" % b2h(ss.smdp_ot.private_bytes(Encoding.DER, PrivateFormat.PKCS8, NoEncryption())))
|
||||||
|
|
||||||
ss.host_id = b'mahlzeit'
|
ss.host_id = b'mahlzeit'
|
||||||
|
|
||||||
@@ -661,7 +664,7 @@ class SmDppHttpServer:
|
|||||||
request.setResponseCode(204)
|
request.setResponseCode(204)
|
||||||
pendingNotification_bin = b64decode(content['pendingNotification'])
|
pendingNotification_bin = b64decode(content['pendingNotification'])
|
||||||
pendingNotification = rsp.asn1.decode('PendingNotification', pendingNotification_bin)
|
pendingNotification = rsp.asn1.decode('PendingNotification', pendingNotification_bin)
|
||||||
print("Rx %s: %s" % pendingNotification)
|
logger.debug("Rx %s: %s" % pendingNotification)
|
||||||
if pendingNotification[0] == 'profileInstallationResult':
|
if pendingNotification[0] == 'profileInstallationResult':
|
||||||
profileInstallRes = pendingNotification[1]
|
profileInstallRes = pendingNotification[1]
|
||||||
pird = profileInstallRes['profileInstallationResultData']
|
pird = profileInstallRes['profileInstallationResultData']
|
||||||
@@ -714,7 +717,7 @@ class SmDppHttpServer:
|
|||||||
@rsp_api_wrapper
|
@rsp_api_wrapper
|
||||||
def cancelSession(self, request: IRequest, content: dict) -> dict:
|
def cancelSession(self, request: IRequest, content: dict) -> dict:
|
||||||
"""See ES9+ CancelSession in SGP.22 Section 5.6.5"""
|
"""See ES9+ CancelSession in SGP.22 Section 5.6.5"""
|
||||||
print("Rx JSON: %s" % content)
|
logger.debug("Rx JSON: %s" % content)
|
||||||
transactionId = content['transactionId']
|
transactionId = content['transactionId']
|
||||||
|
|
||||||
# Verify that the received transactionId is known and relates to an ongoing RSP session
|
# Verify that the received transactionId is known and relates to an ongoing RSP session
|
||||||
@@ -724,7 +727,7 @@ class SmDppHttpServer:
|
|||||||
|
|
||||||
cancelSessionResponse_bin = b64decode(content['cancelSessionResponse'])
|
cancelSessionResponse_bin = b64decode(content['cancelSessionResponse'])
|
||||||
cancelSessionResponse = rsp.asn1.decode('CancelSessionResponse', cancelSessionResponse_bin)
|
cancelSessionResponse = rsp.asn1.decode('CancelSessionResponse', cancelSessionResponse_bin)
|
||||||
print("Rx %s: %s" % cancelSessionResponse)
|
logger.debug("Rx %s: %s" % cancelSessionResponse)
|
||||||
|
|
||||||
if cancelSessionResponse[0] == 'cancelSessionResponseError':
|
if cancelSessionResponse[0] == 'cancelSessionResponseError':
|
||||||
# FIXME: print some error
|
# FIXME: print some error
|
||||||
@@ -760,8 +763,11 @@ def main(argv):
|
|||||||
parser.add_argument("-p", "--port", help="TCP port to bind HTTP to", default=8000)
|
parser.add_argument("-p", "--port", help="TCP port to bind HTTP to", default=8000)
|
||||||
parser.add_argument("-c", "--certdir", help=f"cert subdir relative to {DATA_DIR}", default="certs")
|
parser.add_argument("-c", "--certdir", help=f"cert subdir relative to {DATA_DIR}", default="certs")
|
||||||
parser.add_argument("-s", "--nossl", help="do NOT use ssl", action='store_true', default=False)
|
parser.add_argument("-s", "--nossl", help="do NOT use ssl", action='store_true', default=False)
|
||||||
|
parser.add_argument("-v", "--verbose", help="dump more raw info", action='store_true', default=False)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING)
|
||||||
|
|
||||||
common_cert_path = os.path.join(DATA_DIR, args.certdir)
|
common_cert_path = os.path.join(DATA_DIR, args.certdir)
|
||||||
hs = SmDppHttpServer(server_hostname=HOSTNAME, ci_certs_path=os.path.join(common_cert_path, 'CertificateIssuer'), common_cert_path=common_cert_path, use_brainpool=False)
|
hs = SmDppHttpServer(server_hostname=HOSTNAME, ci_certs_path=os.path.join(common_cert_path, 'CertificateIssuer'), common_cert_path=common_cert_path, use_brainpool=False)
|
||||||
if(args.nossl):
|
if(args.nossl):
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class BspAlgoCrypt(BspAlgo, abc.ABC):
|
|||||||
block_nr = self.block_nr
|
block_nr = self.block_nr
|
||||||
ciphertext = self._encrypt(padded_data)
|
ciphertext = self._encrypt(padded_data)
|
||||||
logger.debug("encrypt(block_nr=%u, s_enc=%s, plaintext=%s, padded=%s) -> %s",
|
logger.debug("encrypt(block_nr=%u, s_enc=%s, plaintext=%s, padded=%s) -> %s",
|
||||||
block_nr, b2h(self.s_enc), b2h(data), b2h(padded_data), b2h(ciphertext))
|
block_nr, b2h(self.s_enc)[:20], b2h(data)[:20], b2h(padded_data)[:20], b2h(ciphertext)[:20])
|
||||||
return ciphertext
|
return ciphertext
|
||||||
|
|
||||||
def decrypt(self, data:bytes) -> bytes:
|
def decrypt(self, data:bytes) -> bytes:
|
||||||
@@ -149,10 +149,20 @@ class BspAlgoMac(BspAlgo, abc.ABC):
|
|||||||
temp_data = self.mac_chain + tag_and_length + data
|
temp_data = self.mac_chain + tag_and_length + data
|
||||||
old_mcv = self.mac_chain
|
old_mcv = self.mac_chain
|
||||||
c_mac = self._auth(temp_data)
|
c_mac = self._auth(temp_data)
|
||||||
|
|
||||||
|
# DEBUG: Show MAC computation details
|
||||||
|
logger.debug(f"MAC_DEBUG: tag=0x{tag:02x}, lcc={lcc}")
|
||||||
|
logger.debug(f"MAC_DEBUG: tag_and_length: {tag_and_length.hex()}")
|
||||||
|
logger.debug(f"MAC_DEBUG: mac_chain[:20]: {old_mcv[:20].hex()}")
|
||||||
|
logger.debug(f"MAC_DEBUG: temp_data[:20]: {temp_data[:20].hex()}")
|
||||||
|
logger.debug(f"MAC_DEBUG: c_mac: {c_mac.hex()}")
|
||||||
|
|
||||||
# The output data is computed by concatenating the following data: the tag, the final length, the result of step 2 and the C-MAC value.
|
# The output data is computed by concatenating the following data: the tag, the final length, the result of step 2 and the C-MAC value.
|
||||||
ret = tag_and_length + data + c_mac
|
ret = tag_and_length + data + c_mac
|
||||||
|
logger.debug(f"MAC_DEBUG: final_output[:20]: {ret[:20].hex()}")
|
||||||
|
|
||||||
logger.debug("auth(tag=0x%x, mcv=%s, s_mac=%s, plaintext=%s, temp=%s) -> %s",
|
logger.debug("auth(tag=0x%x, mcv=%s, s_mac=%s, plaintext=%s, temp=%s) -> %s",
|
||||||
tag, b2h(old_mcv), b2h(self.s_mac), b2h(data), b2h(temp_data), b2h(ret))
|
tag, b2h(old_mcv)[:20], b2h(self.s_mac)[:20], b2h(data)[:20], b2h(temp_data)[:20], b2h(ret)[:20])
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def verify(self, ciphertext: bytes) -> bool:
|
def verify(self, ciphertext: bytes) -> bool:
|
||||||
@@ -204,6 +214,11 @@ def bsp_key_derivation(shared_secret: bytes, key_type: int, key_length: int, hos
|
|||||||
s_enc = out[l:2*l]
|
s_enc = out[l:2*l]
|
||||||
s_mac = out[l*2:3*l]
|
s_mac = out[l*2:3*l]
|
||||||
|
|
||||||
|
logger.debug(f"BSP_KDF_DEBUG: kdf_out = {b2h(out)}")
|
||||||
|
logger.debug(f"BSP_KDF_DEBUG: initial_mcv = {b2h(initial_mac_chaining_value)}")
|
||||||
|
logger.debug(f"BSP_KDF_DEBUG: s_enc = {b2h(s_enc)}")
|
||||||
|
logger.debug(f"BSP_KDF_DEBUG: s_mac = {b2h(s_mac)}")
|
||||||
|
|
||||||
return s_enc, s_mac, initial_mac_chaining_value
|
return s_enc, s_mac, initial_mac_chaining_value
|
||||||
|
|
||||||
|
|
||||||
@@ -231,9 +246,21 @@ class BspInstance:
|
|||||||
"""Encrypt + MAC a single plaintext TLV. Returns the protected ciphertext."""
|
"""Encrypt + MAC a single plaintext TLV. Returns the protected ciphertext."""
|
||||||
assert tag <= 255
|
assert tag <= 255
|
||||||
assert len(plaintext) <= self.max_payload_size
|
assert len(plaintext) <= self.max_payload_size
|
||||||
logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext))
|
|
||||||
|
# DEBUG: Show what we're processing
|
||||||
|
logger.debug(f"BSP_DEBUG: encrypt_and_mac_one(tag=0x{tag:02x}, plaintext_len={len(plaintext)})")
|
||||||
|
logger.debug(f"BSP_DEBUG: plaintext[:20]: {plaintext[:20].hex()}")
|
||||||
|
logger.debug(f"BSP_DEBUG: s_enc[:20]: {self.c_algo.s_enc[:20].hex()}")
|
||||||
|
logger.debug(f"BSP_DEBUG: s_mac[:20]: {self.m_algo.s_mac[:20].hex()}")
|
||||||
|
|
||||||
|
logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext)[:20])
|
||||||
ciphered = self.c_algo.encrypt(plaintext)
|
ciphered = self.c_algo.encrypt(plaintext)
|
||||||
|
logger.debug(f"BSP_DEBUG: ciphered[:20]: {ciphered[:20].hex()}")
|
||||||
|
|
||||||
maced = self.m_algo.auth(tag, ciphered)
|
maced = self.m_algo.auth(tag, ciphered)
|
||||||
|
logger.debug(f"BSP_DEBUG: final_result[:20]: {maced[:20].hex()}")
|
||||||
|
logger.debug(f"BSP_DEBUG: final_result_len: {len(maced)}")
|
||||||
|
|
||||||
return maced
|
return maced
|
||||||
|
|
||||||
def encrypt_and_mac(self, tag: int, plaintext:bytes) -> List[bytes]:
|
def encrypt_and_mac(self, tag: int, plaintext:bytes) -> List[bytes]:
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import pySim.esim.rsp as rsp
|
|||||||
from pySim.esim.bsp import BspInstance
|
from pySim.esim.bsp import BspInstance
|
||||||
from pySim.esim import PMO
|
from pySim.esim import PMO
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Given that GSMA RSP uses ASN.1 in a very weird way, we actually cannot encode the full data type before
|
# Given that GSMA RSP uses ASN.1 in a very weird way, we actually cannot encode the full data type before
|
||||||
# signing, but we have to build parts of it separately first, then sign that, so we can put the signature
|
# signing, but we have to build parts of it separately first, then sign that, so we can put the signature
|
||||||
# into the same sequence as the signed data. We use the existing pySim TLV code for this.
|
# into the same sequence as the signed data. We use the existing pySim TLV code for this.
|
||||||
@@ -196,8 +199,12 @@ class BoundProfilePackage(ProfilePackage):
|
|||||||
# 'initialiseSecureChannelRequest'
|
# 'initialiseSecureChannelRequest'
|
||||||
bpp_seq = rsp.asn1.encode('InitialiseSecureChannelRequest', iscr)
|
bpp_seq = rsp.asn1.encode('InitialiseSecureChannelRequest', iscr)
|
||||||
# firstSequenceOf87
|
# firstSequenceOf87
|
||||||
|
logger.debug(f"BPP_ENCODE_DEBUG: Encrypting ConfigureISDP with BSP keys")
|
||||||
|
logger.debug(f"BPP_ENCODE_DEBUG: BSP S-ENC: {bsp.c_algo.s_enc.hex()}")
|
||||||
|
logger.debug(f"BPP_ENCODE_DEBUG: BSP S-MAC: {bsp.m_algo.s_mac.hex()}")
|
||||||
bpp_seq += encode_seq(0xa0, bsp.encrypt_and_mac(0x87, conf_idsp_bin))
|
bpp_seq += encode_seq(0xa0, bsp.encrypt_and_mac(0x87, conf_idsp_bin))
|
||||||
# sequenceOF88
|
# sequenceOF88
|
||||||
|
logger.debug(f"BPP_ENCODE_DEBUG: MAC-only StoreMetadata with BSP keys")
|
||||||
bpp_seq += encode_seq(0xa1, bsp.mac_only(0x88, smr_bin))
|
bpp_seq += encode_seq(0xa1, bsp.mac_only(0x88, smr_bin))
|
||||||
|
|
||||||
if self.ppp: # we have to use session keys
|
if self.ppp: # we have to use session keys
|
||||||
|
|||||||
Reference in New Issue
Block a user