[bug] Use BrowserLikePolicyForHTTPS for checking
[leap_pycommon.git] / src / leap / common / certs.py
1 # -*- coding: utf-8 -*-
2 # certs.py
3 # Copyright (C) 2013 LEAP
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 """
19 Implements cert checks and helpers
20 """
21
22 import os
23 import time
24 import logging
25
26 from OpenSSL import crypto
27 from dateutil.parser import parse as dateparse
28
29 from leap.common.check import leap_assert
30
31 logger = logging.getLogger(__name__)
32
33
34 def get_cert_from_string(string):
35     """
36     Returns the x509 from the contents of this string
37
38     :param string: certificate contents as downloaded
39     :type string: str
40
41     :return: x509 or None
42     """
43     leap_assert(string, "We need something to load")
44
45     x509 = None
46     try:
47         x509 = crypto.load_certificate(crypto.FILETYPE_PEM, string)
48     except Exception as e:
49         logger.error("Something went wrong while loading the certificate: %r"
50                      % (e,))
51     return x509
52
53
54 def get_privatekey_from_string(string):
55     """
56     Returns the private key from the contents of this string
57
58     :param string: private key contents as downloaded
59     :type string: str
60
61     :return: private key or None
62     """
63     leap_assert(string, "We need something to load")
64
65     pkey = None
66     try:
67         pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, string)
68     except Exception as e:
69         logger.error("Something went wrong while loading the certificate: %r"
70                      % (e,))
71     return pkey
72
73
74 def get_digest(cert_data, method):
75     """
76     Returns the digest for the cert_data using the method specified
77
78     :param cert_data: certificate data in string form
79     :type cert_data: str
80     :param method: method to be used for digest
81     :type method: str
82
83     :rtype: str
84     """
85     x509 = get_cert_from_string(cert_data)
86     digest = x509.digest(method).replace(":", "").lower()
87
88     return digest
89
90
91 def can_load_cert_and_pkey(string):
92     """
93     Loads certificate and private key from a buffer, returns True if
94     everything went well, False otherwise
95
96     :param string: buffer containing the cert and private key
97     :type string: str or any kind of buffer
98
99     :rtype: bool
100     """
101     can_load = True
102
103     try:
104         cert = get_cert_from_string(string)
105         key = get_privatekey_from_string(string)
106
107         leap_assert(cert, 'The certificate could not be loaded')
108         leap_assert(key, 'The private key could not be loaded')
109     except Exception as e:
110         can_load = False
111         logger.error("Something went wrong while trying to load "
112                      "the certificate: %r" % (e,))
113
114     return can_load
115
116
117 def is_valid_pemfile(cert):
118     """
119     Checks that the passed string is a valid pem certificate
120
121     :param cert: String containing pem content
122     :type cert: str
123
124     :rtype: bool
125     """
126     leap_assert(cert, "We need a cert to load")
127
128     return can_load_cert_and_pkey(cert)
129
130
131 def get_cert_time_boundaries(certdata):
132     """
133     Return the time boundaries for the given certificate.
134     The returned values are UTC/GMT time.struct_time objects
135
136     :param certdata: the certificate contents
137     :type certdata: str
138
139     :rtype: tuple (from, to)
140     """
141     cert = get_cert_from_string(certdata)
142     leap_assert(cert, 'There was a problem loading the certificate')
143
144     fromts, tots = (cert.get_notBefore(), cert.get_notAfter())
145     from_ = dateparse(fromts).timetuple()
146     to_ = dateparse(tots).timetuple()
147
148     return from_, to_
149
150
151 def should_redownload(certfile, now=time.gmtime):
152     """
153     Returns True if any of the checks don't pass, False otherwise
154
155     :param certfile: path to certificate
156     :type certfile: str
157     :param now: current date function, ONLY USED FOR TESTING
158
159     :rtype: bool
160     """
161     exists = os.path.isfile(certfile)
162
163     if not exists:
164         return True
165
166     certdata = None
167     try:
168         with open(certfile, "r") as f:
169             certdata = f.read()
170             if not is_valid_pemfile(certdata):
171                 return True
172     except:
173         return True
174
175     valid_from, valid_to = get_cert_time_boundaries(certdata)
176
177     if not (valid_from < now() < valid_to):
178         return True
179
180     return False
181
182
183 def get_compatible_ssl_context_factory(cert_path=None):
184     import twisted
185     cert = None
186     if twisted.version.base() > '14.0.1':
187         from twisted.web.client import BrowserLikePolicyForHTTPS
188         from twisted.internet import ssl
189         if cert_path:
190             cert = ssl.Certificate.loadPEM(open(cert_path).read())
191         policy = BrowserLikePolicyForHTTPS(cert)
192         return policy
193     else:
194         raise Exception(("""
195             Twisted 14.0.2 is needed in order to have secure Client Web SSL Contexts, not %s
196             See: http://twistedmatrix.com/trac/ticket/7647
197             """) % (twisted.version.base()))