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