LNRD.CLL
LNRD.CLL

Reputation: 385

Permission Denied 13 during Soap client method invoke. Python Zeep .pem

I am trying to invoke a service starting within the following .PEM verified zeep client session:

from requests import Session
from zeep import Client
from zeep.settings import Settings
from zeep.transports import Transport
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.hazmat.primitives import serialization

session = Session()
session.verify = True
private_key, certificate, additional_certificates = pkcs12.load_key_and_certificates(
    open(pfx_cert_path, "rb").read(), cert_psw_b)
key_tmpfile = tempfile.NamedTemporaryFile(dir="C:/Users/USERA/Anaconda3/envs/env/"
key_tmpfile.write(
    private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    )
)
key_tmpfile.flush()
cert_tmpfile=tempfile.NamedTemporaryFile(dir="C:/Users/USERA/Anaconda3/envs/env/")
cert_tmpfile.write(
    certificate.public_bytes(serialization.Encoding.PEM),
)
cert_tmpfile.flush()
session.cert = cert_tmpfile.name, key_tmpfile.name

settings = Settings(raw_response=True)
soap_client = Client(wsdl_url, transport=Transport(session=session), settings=settings)

The content I am sending contains both a SOAP-Body and a SOAP-Header with dynamic values. I do this similarly to: soap_client.service.UploadMethod({"body_field":"body_value","_soapheaders":{"head_field":"head_value"}}) This yields the following queue of errors which ends up to a Permission Denied while accessing the temporary file in read mode by urllib3.

  File "C:\Users\USERA\Anaconda3\envs\env\lib\site-packages\urllib3\connection.py", line 419, in connect
    self.sock = ssl_wrap_socket(
  File "C:\Users\USERA\Anaconda3\envs\env\lib\site-packages\urllib3\util\ssl_.py", line 413, in ssl_wrap_socket
if keyfile and key_password is None and _is_key_file_encrypted(keyfile):
  File "C:\Users\USERA\Anaconda3\envs\env\lib\site-packages\urllib3\util\ssl_.py", line 472, in _is_key_file_encrypted
with open(key_file, "r") as f:
PermissionError: [Errno 13] Permission denied:

I have attempted different locations. I have also tried doing this with Suds client or relying directly on requests to make the soap call but I am not able to load the .pem certificate and key as done above. At the moment I managed to simply do that in C# as below:

    CLIENT client = new CLIENT(); // Project -> Add Service Reference -> Advanced -> Add Web Reference -> https://service_domain/service.asmx -> Rename "CLIENT"
    
    string cert_path = "C:/PATH/Certificates/PKCS12_Credential.pfx";
    string cert_psw_path = "C:/PATH/Certificates/psw_pfx.txt";
    X509Certificate2 certificate = new X509Certificate2(cert_path, File.OpenText(cert_psw_path).ReadLine());
    client.ClientCertificates.Add(certificate);
    client.Login("", "");  // Which loads the soap headers
    string xml_body = "C:/PATH/Body.txt";
    client.UploadMethod(File.ReadAllText(xml_body));
    

Does anyone have an idea on how to workaround that? Maybe by changing urllib version or by using an alternative way of passing the certificates directly as bytes-string and not by pathfile?

Thanks in advance.

Upvotes: 0

Views: 110

Answers (1)

Asad
Asad

Reputation: 508

It seems like you're running into a permission issue when trying to access the temporary file created by tempfile.NamedTemporaryFile in Python on Windows. This is a common issue faced by users when using this function on Windows as the OS doesn't allow multiple processes to open the same file. The urllib3 library opens the file in read mode, which results in a PermissionError as the temp file is still open in your Python code.

To work around this issue, you can avoid creating temporary files and load the certificate and key directly from memory. This can be done by using the load_cert_chain() method, which accepts binary data for the certificate and key, rather than file paths.

You might convert your private key and certificate to strings with the private_bytes and public_bytes methods, decode them to UTF-8, and then supply the string to ssl context using ssl.SSLContext(). Below, please find a sample implementation without the tempfile usage, using Zeep’s low-level built-in support for supplying a custom transport:

import ssl
from zeep import Client, Settings, Plugin
from zeep.transports import Transport

from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.hazmat.primitives import serialization

private_key, certificate, additional_certificates = pkcs12.load_key_and_certificates(
    open(pfx_cert_path, "rb").read(), cert_psw_b)

context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)

context.load_cert_chain(
    certfile=certificate.public_bytes(serialization.Encoding.PEM).decode('utf-8'), 
    keyfile=private_key.private_bytes(
        encoding=serialization.Encoding.PEM, 
        format=serialization.PrivateFormat.PKCS8, 
        encryption_algorithm=serialization.NoEncryption()
    ).decode('utf-8'))

settings = Settings(strict=True, xml_huge_tree=True)
client = Client(wsdl_url, 
                plugins=[MyLoggingPlugin()],
                transport=Transport(session=session, timeout=10, operation_timeout=10, ssl_context=context), 
                settings=settings)

response = client.service.MyMethod()

Please take note that the ssl_context parameter is used during the creation of the Transport object. This will allow you to pass your SSL context containing your certificate and key to communicate with the web service.

Also, please remember to replace wsdl_url and MyMethod() with your own Web Service details. Note that session needs to be properly initialized and configured as well.

This approach should avoid the PermissionError you're facing, as it bypasses the need to create a temporary file and loads the certificate and key directly into the SSL context. Remember to keep these strings safe because they will contain very sensitive key and certificate data.

Upvotes: 0

Related Questions