1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """Utility functions for X509.
22
23 """
24
25 import time
26 import OpenSSL
27 import re
28 import datetime
29 import calendar
30 import errno
31 import logging
32
33 from ganeti import errors
34 from ganeti import constants
35 from ganeti import pathutils
36
37 from ganeti.utils import text as utils_text
38 from ganeti.utils import io as utils_io
39 from ganeti.utils import hash as utils_hash
40
41
42 HEX_CHAR_RE = r"[a-zA-Z0-9]"
43 VALID_X509_SIGNATURE_SALT = re.compile("^%s+$" % HEX_CHAR_RE, re.S)
44 X509_SIGNATURE = re.compile(r"^%s:\s*(?P<salt>%s+)/(?P<sign>%s+)$" %
45 (re.escape(constants.X509_CERT_SIGNATURE_HEADER),
46 HEX_CHAR_RE, HEX_CHAR_RE),
47 re.S | re.I)
48
49
50 (CERT_WARNING,
51 CERT_ERROR) = range(1, 3)
52
53
54 _ASN1_TIME_REGEX = re.compile(r"^(\d+)([-+]\d\d)(\d\d)$")
55
56
58 """Parses an ASN1 GENERALIZEDTIME timestamp as used by pyOpenSSL.
59
60 @type value: string
61 @param value: ASN1 GENERALIZEDTIME timestamp
62 @return: Seconds since the Epoch (1970-01-01 00:00:00 UTC)
63
64 """
65 m = _ASN1_TIME_REGEX.match(value)
66 if m:
67
68 asn1time = m.group(1)
69 hours = int(m.group(2))
70 minutes = int(m.group(3))
71 utcoffset = (60 * hours) + minutes
72 else:
73 if not value.endswith("Z"):
74 raise ValueError("Missing timezone")
75 asn1time = value[:-1]
76 utcoffset = 0
77
78 parsed = time.strptime(asn1time, "%Y%m%d%H%M%S")
79
80 tt = datetime.datetime(*(parsed[:7])) - datetime.timedelta(minutes=utcoffset)
81
82 return calendar.timegm(tt.utctimetuple())
83
84
86 """Returns the validity period of the certificate.
87
88 @type cert: OpenSSL.crypto.X509
89 @param cert: X509 certificate object
90
91 """
92
93
94 try:
95 get_notbefore_fn = cert.get_notBefore
96 except AttributeError:
97 not_before = None
98 else:
99 not_before_asn1 = get_notbefore_fn()
100
101 if not_before_asn1 is None:
102 not_before = None
103 else:
104 not_before = _ParseAsn1Generalizedtime(not_before_asn1)
105
106 try:
107 get_notafter_fn = cert.get_notAfter
108 except AttributeError:
109 not_after = None
110 else:
111 not_after_asn1 = get_notafter_fn()
112
113 if not_after_asn1 is None:
114 not_after = None
115 else:
116 not_after = _ParseAsn1Generalizedtime(not_after_asn1)
117
118 return (not_before, not_after)
119
120
123 """Verifies certificate validity.
124
125 @type expired: bool
126 @param expired: Whether pyOpenSSL considers the certificate as expired
127 @type not_before: number or None
128 @param not_before: Unix timestamp before which certificate is not valid
129 @type not_after: number or None
130 @param not_after: Unix timestamp after which certificate is invalid
131 @type now: number
132 @param now: Current time as Unix timestamp
133 @type warn_days: number or None
134 @param warn_days: How many days before expiration a warning should be reported
135 @type error_days: number or None
136 @param error_days: How many days before expiration an error should be reported
137
138 """
139 if expired:
140 msg = "Certificate is expired"
141
142 if not_before is not None and not_after is not None:
143 msg += (" (valid from %s to %s)" %
144 (utils_text.FormatTime(not_before),
145 utils_text.FormatTime(not_after)))
146 elif not_before is not None:
147 msg += " (valid from %s)" % utils_text.FormatTime(not_before)
148 elif not_after is not None:
149 msg += " (valid until %s)" % utils_text.FormatTime(not_after)
150
151 return (CERT_ERROR, msg)
152
153 elif not_before is not None and not_before > now:
154 return (CERT_WARNING,
155 "Certificate not yet valid (valid from %s)" %
156 utils_text.FormatTime(not_before))
157
158 elif not_after is not None:
159 remaining_days = int((not_after - now) / (24 * 3600))
160
161 msg = "Certificate expires in about %d days" % remaining_days
162
163 if error_days is not None and remaining_days <= error_days:
164 return (CERT_ERROR, msg)
165
166 if warn_days is not None and remaining_days <= warn_days:
167 return (CERT_WARNING, msg)
168
169 return (None, None)
170
171
173 """Verifies a certificate for LUClusterVerify.
174
175 @type cert: OpenSSL.crypto.X509
176 @param cert: X509 certificate object
177 @type warn_days: number or None
178 @param warn_days: How many days before expiration a warning should be reported
179 @type error_days: number or None
180 @param error_days: How many days before expiration an error should be reported
181
182 """
183
184 (not_before, not_after) = GetX509CertValidity(cert)
185
186 now = time.time() + constants.NODE_MAX_CLOCK_SKEW
187
188 return _VerifyCertificateInner(cert.has_expired(), not_before, not_after,
189 now, warn_days, error_days)
190
191
193 """Sign a X509 certificate.
194
195 An RFC822-like signature header is added in front of the certificate.
196
197 @type cert: OpenSSL.crypto.X509
198 @param cert: X509 certificate object
199 @type key: string
200 @param key: Key for HMAC
201 @type salt: string
202 @param salt: Salt for HMAC
203 @rtype: string
204 @return: Serialized and signed certificate in PEM format
205
206 """
207 if not VALID_X509_SIGNATURE_SALT.match(salt):
208 raise errors.GenericError("Invalid salt: %r" % salt)
209
210
211 cert_pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
212
213 return ("%s: %s/%s\n\n%s" %
214 (constants.X509_CERT_SIGNATURE_HEADER, salt,
215 utils_hash.Sha1Hmac(key, cert_pem, salt=salt),
216 cert_pem))
217
218
220 """Helper function to extract signature from X509 certificate.
221
222 """
223
224 for line in cert_pem.splitlines():
225 if line.startswith("---"):
226 break
227
228 m = X509_SIGNATURE.match(line.strip())
229 if m:
230 return (m.group("salt"), m.group("sign"))
231
232 raise errors.GenericError("X509 certificate signature is missing")
233
234
236 """Verifies a signed X509 certificate.
237
238 @type cert_pem: string
239 @param cert_pem: Certificate in PEM format and with signature header
240 @type key: string
241 @param key: Key for HMAC
242 @rtype: tuple; (OpenSSL.crypto.X509, string)
243 @return: X509 certificate object and salt
244
245 """
246 (salt, signature) = _ExtractX509CertificateSignature(cert_pem)
247
248
249 (cert, sane_pem) = ExtractX509Certificate(cert_pem)
250
251 if not utils_hash.VerifySha1Hmac(key, sane_pem, signature, salt=salt):
252 raise errors.GenericError("X509 certificate signature is invalid")
253
254 return (cert, salt)
255
256
258 """Generates a self-signed X509 certificate.
259
260 @type common_name: string
261 @param common_name: commonName value
262 @type validity: int
263 @param validity: Validity for certificate in seconds
264 @return: a tuple of strings containing the PEM-encoded private key and
265 certificate
266
267 """
268
269 key = OpenSSL.crypto.PKey()
270 key.generate_key(OpenSSL.crypto.TYPE_RSA, constants.RSA_KEY_BITS)
271
272
273 cert = OpenSSL.crypto.X509()
274 if common_name:
275 cert.get_subject().CN = common_name
276 cert.set_serial_number(1)
277 cert.gmtime_adj_notBefore(0)
278 cert.gmtime_adj_notAfter(validity)
279 cert.set_issuer(cert.get_subject())
280 cert.set_pubkey(key)
281 cert.sign(key, constants.X509_CERT_SIGN_DIGEST)
282
283 key_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
284 cert_pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
285
286 return (key_pem, cert_pem)
287
288
291 """Legacy function to generate self-signed X509 certificate.
292
293 @type filename: str
294 @param filename: path to write certificate to
295 @type common_name: string
296 @param common_name: commonName value
297 @type validity: int
298 @param validity: validity of certificate in number of days
299 @return: a tuple of strings containing the PEM-encoded private key and
300 certificate
301
302 """
303
304
305
306 (key_pem, cert_pem) = GenerateSelfSignedX509Cert(common_name,
307 validity * 24 * 60 * 60)
308
309 utils_io.WriteFile(filename, mode=0400, data=key_pem + cert_pem)
310 return (key_pem, cert_pem)
311
312
314 """Extracts the certificate from a PEM-formatted string.
315
316 @type pem: string
317 @rtype: tuple; (OpenSSL.X509 object, string)
318 @return: Certificate object and PEM-formatted certificate
319
320 """
321 cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem)
322
323 return (cert,
324 OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert))
325
326
328 """Get function for verifying certificate with a certain private key.
329
330 @type key: OpenSSL.crypto.PKey
331 @param key: Private key object
332 @type cert: OpenSSL.crypto.X509
333 @param cert: X509 certificate object
334 @rtype: callable
335 @return: Callable doing the actual check; will raise C{OpenSSL.SSL.Error} if
336 certificate is not signed by given private key
337
338 """
339 ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
340 ctx.use_privatekey(key)
341 ctx.use_certificate(cert)
342
343 return ctx.check_privatekey
344
345
347 """Checks the local node daemon certificate against given certificate.
348
349 Both certificates must be signed with the same key (as stored in the local
350 L{pathutils.NODED_CERT_FILE} file). No error is raised if no local
351 certificate can be found.
352
353 @type cert: OpenSSL.crypto.X509
354 @param cert: X509 certificate object
355 @raise errors.X509CertError: When an error related to X509 occurred
356 @raise errors.GenericError: When the verification failed
357
358 """
359 try:
360 noded_pem = utils_io.ReadFile(_noded_cert_file)
361 except EnvironmentError, err:
362 if err.errno != errno.ENOENT:
363 raise
364
365 logging.debug("Node certificate file '%s' was not found", _noded_cert_file)
366 return
367
368 try:
369 noded_cert = \
370 OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, noded_pem)
371 except Exception, err:
372 raise errors.X509CertError(_noded_cert_file,
373 "Unable to load certificate: %s" % err)
374
375 try:
376 noded_key = \
377 OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, noded_pem)
378 except Exception, err:
379 raise errors.X509CertError(_noded_cert_file,
380 "Unable to load private key: %s" % err)
381
382
383 check_fn = PrepareX509CertKeyCheck(noded_cert, noded_key)
384 try:
385 check_fn()
386 except OpenSSL.SSL.Error:
387
388
389 raise errors.X509CertError(_noded_cert_file,
390 "Certificate does not match with private key")
391
392
393 check_fn = PrepareX509CertKeyCheck(cert, noded_key)
394 try:
395 check_fn()
396 except OpenSSL.SSL.Error:
397 raise errors.GenericError("Given cluster certificate does not match"
398 " local key")
399