Python으로 SSL 인증서 유효성 검사


85

HTTPS를 통해 회사 인트라넷의 여러 사이트에 연결하고 SSL 인증서가 유효한지 확인하는 스크립트를 작성해야합니다. 만료되지 않았는지, 올바른 주소로 발급되었는지 등. 이러한 사이트에 대해 자체 내부 기업 인증 기관을 사용하므로 인증서를 확인할 CA의 공개 키가 있습니다.

Python은 기본적으로 HTTPS를 사용할 때 SSL 인증서를 수락하고 사용하므로 인증서가 유효하지 않더라도 urllib2 및 Twisted와 같은 Python 라이브러리는 인증서를 기꺼이 사용합니다.

HTTPS를 통해 사이트에 연결하고 이러한 방식으로 인증서를 확인할 수있는 좋은 라이브러리가 있습니까?

Python에서 인증서를 어떻게 확인합니까?


10
Twisted에 대한 귀하의 의견이 잘못되었습니다. Twisted는 Python의 내장 SSL 지원이 아닌 pyopenssl을 사용합니다. HTTP 클라이언트에서 기본적으로 HTTPS 인증서의 유효성을 검사하지 않지만 getPage 및 downloadPage에 "contextFactory"인수를 사용하여 유효성 검사 컨텍스트 팩토리를 구성 할 수 있습니다. 대조적으로, 내 지식으로는 내장 "ssl"모듈이 인증서 유효성 검사를 수행하도록 설득 할 수있는 방법이 없습니다.
Glyph

4
Python 2.6 이상에서 SSL 모듈을 사용하면 자체 인증서 유효성 검사기를 작성할 수 있습니다. 최적은 아니지만 실행 가능합니다.
Heikki Toivonen

3
상황이 바뀌어 이제 Python은 기본적으로 인증서의 유효성을 검사합니다. 아래에 새로운 답변을 추가했습니다.
Dr. Jan-Philip Gehrcke 2015

Twisted의 경우도 상황이 변경되었습니다 (사실 Python에서는 다소 이전에 변경되었습니다). 버전 14.0 treq이상 을 사용하는 경우 twisted.web.client.AgentTwisted는 기본적으로 인증서를 확인합니다.
Glyph

답변:


19

릴리스 버전 2.7.9 / 3.4.3부터 Python 은 기본적으로 인증서 유효성 검사를 수행합니다.

이것은 읽을만한 가치가있는 PEP 467에서 제안되었습니다 : https://www.python.org/dev/peps/pep-0476/

변경 사항은 모든 관련 stdlib 모듈 (urllib / urllib2, http, httplib)에 영향을줍니다.

관련 문서 :

https://docs.python.org/2/library/httplib.html#httplib.HTTPSConnection

이 클래스는 이제 기본적으로 필요한 모든 인증서 및 호스트 이름 검사를 수행합니다. 확인되지 않은 이전 동작으로 되돌리려면 ssl._create_unverified_context ()를 context 매개 변수에 전달할 수 있습니다.

https://docs.python.org/3/library/http.client.html#http.client.HTTPSConnection

버전 3.4.3에서 변경 :이 클래스는 이제 기본적으로 필요한 모든 인증서 및 호스트 이름 검사를 수행합니다. 확인되지 않은 이전 동작으로 되돌리려면 ssl._create_unverified_context ()를 context 매개 변수에 전달할 수 있습니다.

새로운 기본 제공 확인은 시스템에서 제공하는 인증서 데이터베이스를 기반으로 합니다. 반대로 요청 패키지는 자체 인증서 번들을 제공합니다. 두 접근 방식의 장단점 은 PEP 476신뢰 데이터베이스 섹션 에서 설명합니다 .


이전 버전의 Python에 대한 인증서 확인을 보장하는 솔루션이 있습니까? 항상 파이썬 버전을 업그레이드 할 수있는 것은 아닙니다.
vaab apr

해지 된 인증서의 유효성을 검사하지 않습니다. 예 : revoked.badssl.com
Raz

HTTPSConnection수업 은 필수 인가요? 나는 SSLSocket. 어떻게 검증을 할 수 SSLSocket있습니까? 여기에pyopenssl 설명 된대로 사용하여 명시 적으로 유효성을 검사해야 합니까?
anir

31

match_hostname()Python 3.2 ssl패키지 의 함수를 이전 버전의 Python에서 사용할 수 있도록 Python Package Index에 배포를 추가했습니다 .

http://pypi.python.org/pypi/backports.ssl_match_hostname/

다음과 같이 설치할 수 있습니다.

pip install backports.ssl_match_hostname

또는 프로젝트의 setup.py. 어느 쪽이든 다음과 같이 사용할 수 있습니다.

from backports.ssl_match_hostname import match_hostname, CertificateError
...
sslsock = ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_SSLv3,
                      cert_reqs=ssl.CERT_REQUIRED, ca_certs=...)
try:
    match_hostname(sslsock.getpeercert(), hostname)
except CertificateError, ce:
    ...

1
누락 된 항목이 있습니다 ... 위의 빈칸을 채우거나 완전한 예를 제공 할 수 있습니까 (Google과 같은 사이트의 경우)?
smholloway

예제는 Google에 액세스하는 데 사용하는 라이브러리에 따라 다르게 보입니다. 다른 라이브러리는 SSL 소켓을 다른 위치에 배치 getpeercert()하고 출력을 match_hostname().
Brandon Rhodes

12
나는 누구나 이것을 사용해야하는 파이썬 대신에 부끄럽다. 기본적으로 인증서를 확인하지 않는 Python의 내장 SSL HTTPS 라이브러리는 완전히 미친 짓이며 결과적으로 현재 얼마나 많은 안전하지 않은 시스템이 있는지 상상하기가 어렵습니다.
Glenn Maynard


26

Twisted를 사용하여 인증서를 확인할 수 있습니다. 주요 API는 CertificateOptions 이며 listenSSLstartTLScontextFactory 와 같은 다양한 함수에 대한 인수로 제공 될 수 있습니다 .

불행히도 Python이나 Twisted는 실제로 HTTPS 유효성 검사를 수행하는 데 필요한 CA 인증서 더미와 HTTPS 유효성 검사 논리를 제공하지 않습니다. PyOpenSSL의 제한 으로 인해 아직 완전히 올바르게 수행 할 수는 없지만 거의 모든 인증서에 주제 commonName이 포함되어 있기 때문에 충분히 가까워 질 수 있습니다.

다음은 와일드 카드 및 subjectAltName 확장을 무시하고 대부분의 Ubuntu 배포판에서 'ca-certificates'패키지에있는 인증 기관 인증서를 사용하는 검증 Twisted HTTPS 클라이언트의 순진한 샘플 구현입니다. 좋아하는 유효하고 유효하지 않은 인증서 사이트에서 시도해보십시오. :).

import os
import glob
from OpenSSL.SSL import Context, TLSv1_METHOD, VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT, OP_NO_SSLv2
from OpenSSL.crypto import load_certificate, FILETYPE_PEM
from twisted.python.urlpath import URLPath
from twisted.internet.ssl import ContextFactory
from twisted.internet import reactor
from twisted.web.client import getPage
certificateAuthorityMap = {}
for certFileName in glob.glob("/etc/ssl/certs/*.pem"):
    # There might be some dead symlinks in there, so let's make sure it's real.
    if os.path.exists(certFileName):
        data = open(certFileName).read()
        x509 = load_certificate(FILETYPE_PEM, data)
        digest = x509.digest('sha1')
        # Now, de-duplicate in case the same cert has multiple names.
        certificateAuthorityMap[digest] = x509
class HTTPSVerifyingContextFactory(ContextFactory):
    def __init__(self, hostname):
        self.hostname = hostname
    isClient = True
    def getContext(self):
        ctx = Context(TLSv1_METHOD)
        store = ctx.get_cert_store()
        for value in certificateAuthorityMap.values():
            store.add_cert(value)
        ctx.set_verify(VERIFY_PEER | VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
        ctx.set_options(OP_NO_SSLv2)
        return ctx
    def verifyHostname(self, connection, x509, errno, depth, preverifyOK):
        if preverifyOK:
            if self.hostname != x509.get_subject().commonName:
                return False
        return preverifyOK
def secureGet(url):
    return getPage(url, HTTPSVerifyingContextFactory(URLPath.fromString(url).netloc))
def done(result):
    print 'Done!', len(result)
secureGet("https://google.com/").addCallback(done)
reactor.run()

비 차단으로 만들 수 있습니까?
sean riley

감사; 나는 이것을 읽고 이해 했으므로 이제 한 가지 메모가 있습니다. 확인 콜백은 오류가 없으면 True를 반환하고 오류가 있으면 False를 반환해야합니다. commonName이 localhost가 아닌 경우 코드는 기본적으로 오류를 반환합니다. 어떤 경우에는이 작업을 수행하는 것이 합리적이지만 그게 의도 한 것인지 확실하지 않습니다. 나는이 답변의 미래 독자를 위해 이것에 대한 의견을 남길 것이라고 생각했습니다.
Eli Courtwright

이 경우 "self.hostname"은 "localhost"가 아닙니다. 참고 URLPath(url).netloc: 이는 secureGet에 전달 된 URL의 호스트 부분을 의미합니다. 즉, 주체의 commonName이 호출자가 요청한 것과 동일한 지 확인하는 것입니다.
Glyph

이 테스트 코드의 버전을 실행하고 있으며 Firefox, wget 및 Chrome을 사용하여 테스트 HTTPS 서버에 도달했습니다. 내 테스트 실행에서 콜백 verifyHostname이 매 연결마다 3-4 번 호출되는 것을 확인했습니다. 한 번만 실행되지 않는 이유는 무엇입니까?
themaestro

2
URLPath (blah) .netloc 항상 localhost입니다. URLPath .__ init__는 개별 URL 구성 요소를 취하고 전체 URL을 "scheme"으로 전달하고 기본 netloc 'localhost'를 가져옵니다. URLPath.fromString (url) .netloc을 사용하려고했을 것입니다. 불행히도 verifyHostName의 검사가 거꾸로 노출됩니다 https://www.google.com/. 제목 중 하나가 'www.google.com'이기 때문에 거부 하기 시작 하여 함수가 False를 반환합니다. 이름이 일치하면 True (허용됨)를 반환하고 일치하지 않으면 False를 반환한다는 의미 일 수 있습니다.
mzz

25

PycURL 은 이것을 아름답게합니다.

다음은 간단한 예입니다. 그것은 발생합니다 pycurl.error뭔가 비린내 경우 오류 코드와 인간이 읽을 수있는 메시지가있는 튜플을 얻을 경우.

import pycurl

curl = pycurl.Curl()
curl.setopt(pycurl.CAINFO, "myFineCA.crt")
curl.setopt(pycurl.SSL_VERIFYPEER, 1)
curl.setopt(pycurl.SSL_VERIFYHOST, 2)
curl.setopt(pycurl.URL, "https://internal.stuff/")

curl.perform()

결과를 저장할 위치 등과 같은 더 많은 옵션을 구성하고 싶을 것입니다. 그러나 필수가 아닌 것으로 예제를 복잡하게 만들 필요는 없습니다.

발생할 수있는 예외의 예 :

(60, 'Peer certificate cannot be authenticated with known CA certificates')
(51, "common name 'CN=something.else.stuff,O=Example Corp,C=SE' does not match 'internal.stuff'")

내가 유용하다고 생각한 일부 링크는 setopt 및 getinfo에 대한 libcurl-docs입니다.


15

또는 요청 라이브러리 를 사용하여 삶을 더 쉽게 만드십시오 .

import requests
requests.get('https://somesite.com', cert='/path/server.crt', verify=True)

사용법에 대한 몇 마디 더.


10
cert인수는 클라이언트 측 인증서,하지에 대해 확인하는 서버 인증서입니다. verify인수 를 사용하고 싶습니다 .
Paŭlo Ebermann 2014

2
요청 은 기본적으로 유효성을 검사 합니다 . verify더 명시 적이거나 검증을 비활성화하는 것을 제외하고 는 인수 를 사용할 필요가 없습니다 .
Dr. Jan-Philip Gehrcke

1
내부 모듈이 아닙니다. pip 설치 요청을 실행해야합니다
Robert Townley

14

다음은 인증서 유효성 검사를 보여주는 예제 스크립트입니다.

import httplib
import re
import socket
import sys
import urllib2
import ssl

class InvalidCertificateException(httplib.HTTPException, urllib2.URLError):
    def __init__(self, host, cert, reason):
        httplib.HTTPException.__init__(self)
        self.host = host
        self.cert = cert
        self.reason = reason

    def __str__(self):
        return ('Host %s returned an invalid certificate (%s) %s\n' %
                (self.host, self.reason, self.cert))

class CertValidatingHTTPSConnection(httplib.HTTPConnection):
    default_port = httplib.HTTPS_PORT

    def __init__(self, host, port=None, key_file=None, cert_file=None,
                             ca_certs=None, strict=None, **kwargs):
        httplib.HTTPConnection.__init__(self, host, port, strict, **kwargs)
        self.key_file = key_file
        self.cert_file = cert_file
        self.ca_certs = ca_certs
        if self.ca_certs:
            self.cert_reqs = ssl.CERT_REQUIRED
        else:
            self.cert_reqs = ssl.CERT_NONE

    def _GetValidHostsForCert(self, cert):
        if 'subjectAltName' in cert:
            return [x[1] for x in cert['subjectAltName']
                         if x[0].lower() == 'dns']
        else:
            return [x[0][1] for x in cert['subject']
                            if x[0][0].lower() == 'commonname']

    def _ValidateCertificateHostname(self, cert, hostname):
        hosts = self._GetValidHostsForCert(cert)
        for host in hosts:
            host_re = host.replace('.', '\.').replace('*', '[^.]*')
            if re.search('^%s$' % (host_re,), hostname, re.I):
                return True
        return False

    def connect(self):
        sock = socket.create_connection((self.host, self.port))
        self.sock = ssl.wrap_socket(sock, keyfile=self.key_file,
                                          certfile=self.cert_file,
                                          cert_reqs=self.cert_reqs,
                                          ca_certs=self.ca_certs)
        if self.cert_reqs & ssl.CERT_REQUIRED:
            cert = self.sock.getpeercert()
            hostname = self.host.split(':', 0)[0]
            if not self._ValidateCertificateHostname(cert, hostname):
                raise InvalidCertificateException(hostname, cert,
                                                  'hostname mismatch')


class VerifiedHTTPSHandler(urllib2.HTTPSHandler):
    def __init__(self, **kwargs):
        urllib2.AbstractHTTPHandler.__init__(self)
        self._connection_args = kwargs

    def https_open(self, req):
        def http_class_wrapper(host, **kwargs):
            full_kwargs = dict(self._connection_args)
            full_kwargs.update(kwargs)
            return CertValidatingHTTPSConnection(host, **full_kwargs)

        try:
            return self.do_open(http_class_wrapper, req)
        except urllib2.URLError, e:
            if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1:
                raise InvalidCertificateException(req.host, '',
                                                  e.reason.args[1])
            raise

    https_request = urllib2.HTTPSHandler.do_request_

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print "usage: python %s CA_CERT URL" % sys.argv[0]
        exit(2)

    handler = VerifiedHTTPSHandler(ca_certs = sys.argv[1])
    opener = urllib2.build_opener(handler)
    print opener.open(sys.argv[2]).read()

@tonfa : 좋은 캐치; 결국 호스트 이름 검사도 추가했고 내가 사용한 코드를 포함하도록 내 대답을 편집했습니다.
Eli Courtwright

원래 링크 (예 : '이 페이지')에 연결할 수 없습니다. 움직 였나요?
Matt Ball

@Matt : 그렇게 생각하지만 FWIW는 내 테스트 프로그램이 완전하고 독립적 인 작업 예제이기 때문에 원래 링크가 필요하지 않습니다. 귀속을 제공하는 것이 괜찮은 것처럼 보였기 때문에 해당 코드를 작성하는 데 도움이 된 페이지에 연결했습니다. 그러나 더 이상 존재하지 않으므로 링크를 제거하도록 게시물을 편집하겠습니다. 지적 해 주셔서 감사합니다.
Eli Courtwright 2011 년

.NET의 수동 소켓 연결로 인해 프록시 처리기와 같은 추가 처리기에서는 작동하지 않습니다 CertValidatingHTTPSConnection.connect. 자세한 내용 (및 수정 사항) 은이 풀 요청 을 참조하십시오 .
schlamar

2
다음backports.ssl_match_hostname.
schlamar

8

M2Crypto유효성 검사를 할 수 있습니다 . 원하는 경우 Twisted와 함께 M2Crypto를 사용할 수도 있습니다 . Chandler 데스크톱 클라이언트 인증서 유효성 검사를 포함하여 네트워킹에 Twisted를 사용하고 SSL에 M2Crypto를 사용합니다 .

Glyphs 주석에 따르면 M2Crypto는 subjectAltName 필드도 확인하기 때문에 M2Crypto가 현재 pyOpenSSL로 할 수있는 것보다 기본적으로 더 나은 인증서 확인을 수행하는 것처럼 보입니다.

또한 Mozilla Firefox가 Python에서 제공하고 Python SSL 솔루션과 함께 사용할 수있는 인증서얻는 방법에 대해 블로그를 작성했습니다 .


4

Jython은 기본적으로 인증서 확인을 수행하므로 jython과 함께 httplib.HTTPSConnection 등의 표준 라이브러리 모듈을 사용하면 인증서를 확인하고 실패에 대한 예외 (예 : 일치하지 않는 ID, 만료 된 인증서 등)를 제공합니다.

사실, jython이 cpython처럼 작동하도록하려면, 즉 jython이 인증서를 확인하지 않도록하려면 추가 작업을 수행해야합니다.

테스트 단계 등에서 유용 할 수 있기 때문에 자이 썬에서 인증서 검사를 비활성화하는 방법에 대한 블로그 게시물을 작성했습니다.

Java 및 jython에 모든 신뢰 보안 공급자를 설치합니다.
http://jython.xhaus.com/installing-an-all-trusting-security-provider-on-java-and-jython/


2

다음 코드를 사용하면 모든 SSL 유효성 검사 (예 : 날짜 유효성, CA 인증서 체인 ...)의 이점을 누릴 수 있습니다 (예 : 호스트 이름 확인 또는 기타 추가 인증서 확인 단계 수행).

from httplib import HTTPSConnection
import ssl


def create_custom_HTTPSConnection(host):

    def verify_cert(cert, host):
        # Write your code here
        # You can certainly base yourself on ssl.match_hostname
        # Raise ssl.CertificateError if verification fails
        print 'Host:', host
        print 'Peer cert:', cert

    class CustomHTTPSConnection(HTTPSConnection, object):
        def connect(self):
            super(CustomHTTPSConnection, self).connect()
            cert = self.sock.getpeercert()
            verify_cert(cert, host)

    context = ssl.create_default_context()
    context.check_hostname = False
    return CustomHTTPSConnection(host=host, context=context)


if __name__ == '__main__':
    # try expired.badssl.com or self-signed.badssl.com !
    conn = create_custom_HTTPSConnection('badssl.com')
    conn.request('GET', '/')
    conn.getresponse().read()

-1

pyOpenSSL 은 OpenSSL 라이브러리에 대한 인터페이스입니다. 필요한 모든 것을 제공해야합니다.


OpenSSL은 호스트 이름 일치를 수행하지 않습니다. OpenSSL 1.1.0을 위해 계획되었습니다.
jww

-1

나는 같은 문제가 있었지만 타사 종속성을 최소화하고 싶었습니다 (이 일회용 스크립트는 많은 사용자가 실행하기 때문입니다). 내 해결책은 curl호출 을 래핑 하고 종료 코드가 0. 매력처럼 작동했습니다.


나는 pycurl을 사용하는 stackoverflow.com/a/1921551/1228491 이 훨씬 더 나은 해결책 이라고 말하고 싶습니다 .
Marian
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.