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