requests icon indicating copy to clipboard operation
requests copied to clipboard

Client Certificates w/Passphrases?

Open tdussa opened this issue 10 years ago • 32 comments

Hi,

client certificates can be specified with the "cert" parameter. If I pass an encrypted client certificate, the underlying OpenSSL call will query for the corresponding passphrase, but that is not really a feasible way of handling this for a larger session with multiple calls, because the password is obviously not cached in any way.

Is there any way to pass the passphrase to a connection with API calls so it is then passed on to OpenSSL?

Cheers, Toby.

tdussa avatar Mar 30 '15 15:03 tdussa

Hi @tdussa,

In the future, please ask questions on StackOverflow. Quoting from the docs.

cert – (optional) if String, path to ssl client cert file (.pem). If Tuple, (‘cert’, ‘key’) pair.

So if you have the key file, you can use that. @t-8ch can correct me if I'm wrong, but I don't believe we (or urllib3) support sending a passphrase. I think the lack of support is specifically a limitation of the way the SSL module loads verification data.

sigmavirus24 avatar Mar 30 '15 17:03 sigmavirus24

Hi,

THX for your quick response. Admittedly, I was hoping that this would just be a question, but I am afraid that it will actually be a feature request if I read you correctly.

So I have tried to read through the relevant portions of the request and urllib3 code, and I had hoped that I had overlooked some obvious way of passing a passphrase to pyOpenSSL.

Actually, the underlying SSL code DOES support encrypted (password-protected) client certificates; the relevant function that is called by urllib3 is not load_verify_locations but load_cert_chain (which I also think is badly named). So pyOpenSSL does support what I'm looking for, but I haven't been able to figure out a way of actually passing a password argument through requests/urllib3 to pyOpenSSL. I have started thinking about how to best patch this into both requests and urllib3, but the actual code path is not that obvious to me right now.

THX & Cheers, Toby.

tdussa avatar Mar 30 '15 19:03 tdussa

So the appropriate way to do this, would be for me to pick work back up on https://github.com/shazow/urllib3/pull/507 since it would provide the API you're looking for. You would create a SSLContext object and call load_cert_chain yourself with the password and give that to us to use.

sigmavirus24 avatar Mar 30 '15 20:03 sigmavirus24

Sounds about right, yeah. So if that would be possible, that'd be awesome.

THX & Cheers, Toby.

tdussa avatar Mar 31 '15 12:03 tdussa

Is there any estimate as to when this feature might be added?

tdussa avatar Apr 28 '15 09:04 tdussa

@tdussa Not at this time I'm afraid. =(

Lukasa avatar Apr 28 '15 09:04 Lukasa

Bummer. :( Looking forward to it. ;-)

tdussa avatar Apr 28 '15 10:04 tdussa

Waiting for this feature as well. Any estimates?

traut avatar Nov 04 '15 12:11 traut

Nope. There's a big chunk of work to be done for this.

Lukasa avatar Nov 04 '15 12:11 Lukasa

Is it not even in the next milestone?

traut avatar Nov 04 '15 12:11 traut

Not currently, no. We have a finite amount of resources to spend, and the development team is stretched pretty thin across a wide number of projects.

Lukasa avatar Nov 04 '15 13:11 Lukasa

any advice on how to get to urllib3's context.load_cert_chain from requests.Session or prepared request ? I'm looking for a workaround

traut avatar Nov 17 '15 15:11 traut

@traut There isn't a good one, really. You can attempt to use the TransportAdapter's init_poolmanager method to pass objects into urllib3.

Lukasa avatar Nov 17 '15 15:11 Lukasa

any progress on this issue?

traut avatar Sep 14 '16 11:09 traut

@traut Yes. We're a few releases away from users being able to use TransportAdapters to provide SSLContext objects to urllib3, which will resolve this issue.

Lukasa avatar Sep 14 '16 15:09 Lukasa

nice! thanks for the update, @Lukasa

traut avatar Sep 14 '16 15:09 traut

Looks like this is now possible. Here's an example of how I made this work:

https://gist.github.com/aiguofer/1eb881ccf199d4aaa2097d87f93ace6a

aiguofer avatar Feb 13 '17 15:02 aiguofer

My question is very related, but not quite the same. Is there a way to pass the unencrypted certificates and key as raw_bytes or as file objects, rather than file paths? Since for security reasons, one may not want to store the certificates on disk.

amiralia avatar Mar 08 '17 20:03 amiralia

@amiralia Right now the answer is "not easily". The Python standard library ssl module exposes no way to load client certs/keys from bytes: only from files. This is despite the fact that OpenSSL itself does provide these tools.

One way around this is to ensure you're using Requests PyOpenSSL support. If you do that, you can get an SSLContext object that uses PyOpenSSL instead by calling requests.packages.urllib3.util.ssl_.create_urllib3_context(). That will let you call PyOpenSSL methods like use_certificate, which will work with files loaded from memory.

Unfortunately, until the stdlib changes its API, that is the only option.

Lukasa avatar Mar 08 '17 21:03 Lukasa

I tried that, but even with using the following code for injection, and then creating the context as described, the SSL_Context object didn't have use_certificate or use_privatekey methods. import urllib3.contrib.pyopenssl urllib3.contrib.pyopenssl.inject_into_urllib3()

amiralia avatar Mar 09 '17 15:03 amiralia

The object itself does not, but it has a ctx object on it that does. You'll need to look at the actual code in that module to see how it works.

Lukasa avatar Mar 09 '17 15:03 Lukasa

Ok, I got it to work, but there was a very weird bug where the injection overwrote the variables under requests.packages.urllib3.util, but not requests.packages.urllib3.util.ssl_, including the important requests.packages.urllib3.util.ssl_.SSLContext. I resolved it by doing a manual monkey patch in my code of the remaining variables.

amiralia avatar Mar 09 '17 16:03 amiralia

Thanks again everyone!

amiralia avatar Mar 09 '17 16:03 amiralia

Here's an alternative. Re-encode the cert to not require a passphrase.

Run this script passing the .p12 as an argument. It'll prompt for the passphrase and generate a .pem that doesn't need one. Then it asks for a new passphrase (twice, for validation) that's used to build a new .p12. You can leave it blank and the result is a .p12 and .pem that don't require passphrases.

#!/bin/bash -e
np_pem=${1/.p12/_np.pem}
np_p12=${np_pem/pem/p12}
openssl pkcs12 -in $1 -nodes -out $np_pem
echo "Press <CR> twice here for empty passphrase"
openssl pkcs12 -export -in $np_pem  -out $np_p12

bedge avatar May 23 '18 17:05 bedge

Thanks bedge! That's good to know. But for our security requirements, we're mandated by the company to keep passphrases.

amiralia avatar May 23 '18 17:05 amiralia

workaround idea: maybe it's possible to re-encode into a temp file that never hits the disk, yet can be accessed as named file through /dev/fd/N? tempfile.TemporaryFile(), unix only.
Security depends on O_TMPFILE support or unpredictability of file name, and on non-tmpfs filesystems on question whether contents for an unnamed inode might still be flushed out to disk (though not accessible from any directory). See also https://unix.stackexchange.com/questions/74497/single-process-accessible-temporary-file

EDIT: note that other processes by same user can see and read the temp file via a path like /proc/20782/fd/3. (The symlink reads as broken 3 -> '/tmp/#345829 (deleted)' but opening does work. Rescuing deleted but still open files is a deliberate use case of /proc/NN/fd...)
There are of course other attack vectors from processes by same user, e.g. ptrace...

cben avatar May 07 '19 11:05 cben

Here is a solution that I found by taking inspiration from this article:
Creating a Python requests session using a passphrase protected Client side Cert

import ssl
import requests
from requests.adapters import HTTPAdapter

class SSLAdapter(HTTPAdapter):

    def __init__(self, certfile, password):
        self.context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
        self.context.load_cert_chain(certfile=certfile, password=password)
        super().__init__()

    def init_poolmanager(self, *args, **kwargs):
        kwargs['ssl_context'] = self.context
        return super().init_poolmanager(*args, **kwargs)

session = requests.session()
session.mount('https://my_protected_site.com', SSLAdapter('my_certificate.crt', 'my_passphrase'))

qfayet avatar May 19 '19 14:05 qfayet

Yes, that's what I ended up using as well those years ago. It should be doable for requests to wrap that logic in a function call for get_secure_session? Amirali Abdullah

Software Engineering

http://www.facebook.com/Qualtrics http://www.twitter.com/Qualtrics http://www.instagram.com/qualtrics http://www.linkedin.com/company/qualtrics

On Sun, May 19, 2019 at 8:31 AM qfayet [email protected] wrote:

Here is a solution that I found by taking inspiration from this article: https://gist.github.com/aiguofer/1eb881ccf199d4aaa2097d87f93ace6a

import ssl import requests from requests.adapters import HTTPAdapter

class SSLAdapter(HTTPAdapter):

def init_poolmanager(self, *args, **kwargs):
    context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    context.load_cert_chain(certfile='my_certificate.crt',
                            password='my_password')
    kwargs['ssl_context'] = context
    return super().init_poolmanager(*args, **kwargs)

session = requests.session() session.mount('https://my_protected_site.com', SSLAdapter())

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/kennethreitz/requests/issues/2519?email_source=notifications&email_token=AE4W6UK3AZ7QPKBDWM6M7QDPWFQC7A5CNFSM4A64TK5KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODVXDMJQ#issuecomment-493762086, or mute the thread https://github.com/notifications/unsubscribe-auth/AE4W6UNZRK5TI7Z2PI46DRDPWFQC7ANCNFSM4A64TK5A .

amiralia avatar May 19 '19 15:05 amiralia

I believe this is a duplicate of #1573?

fdcds avatar Jun 27 '19 18:06 fdcds

AFAIK, it's still not supported today (at least according to documentation). Looks like it has been requested for a long time. Is there any plan for this ?

yohonet avatar Jan 03 '22 09:01 yohonet

Would you be willing to accept pull requests that implement this?

tbennett6421 avatar Jul 19 '22 19:07 tbennett6421