x509 function for creating signed certs
[ganeti-github.git] / lib / utils / x509.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2010, 2011, 2012 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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 # Certificate verification results
60 (CERT_WARNING,
61 CERT_ERROR) = range(1, 3)
62
63 #: ASN1 time regexp
64 _ASN1_TIME_REGEX = re.compile(r"^(\d+)([-+]\d\d)(\d\d)$")
65
66
67 def _ParseAsn1Generalizedtime(value):
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 # We have an offset
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
95 def GetX509CertValidity(cert):
96 """Returns the validity period of the certificate.
97
98 @type cert: OpenSSL.crypto.X509
99 @param cert: X509 certificate object
100
101 """
102 # The get_notBefore and get_notAfter functions are only supported in
103 # pyOpenSSL 0.7 and above.
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
131 def _VerifyCertificateInner(expired, not_before, not_after, now,
132 warn_days, error_days):
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
182 def VerifyX509Certificate(cert, warn_days, error_days):
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 # Depending on the pyOpenSSL version, this can just return (None, None)
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
202 def SignX509Certificate(cert, key, salt):
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 # Dumping as PEM here ensures the certificate is in a sane format
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
229 def _ExtractX509CertificateSignature(cert_pem):
230 """Helper function to extract signature from X509 certificate.
231
232 """
233 # Extract signature from original PEM data
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
245 def LoadSignedX509Certificate(cert_pem, key):
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 # Load and dump certificate to ensure it's in a sane format
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
267 def GenerateSelfSignedX509Cert(common_name, validity, serial_no):
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 # Create private and public key
279 key = OpenSSL.crypto.PKey()
280 key.generate_key(OpenSSL.crypto.TYPE_RSA, constants.RSA_KEY_BITS)
281
282 # Create self-signed certificate
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
299 def GenerateSelfSignedSslCert(filename, serial_no,
300 common_name=constants.X509_CERT_CN,
301 validity=constants.X509_CERT_DEFAULT_VALIDITY,
302 uid=-1, gid=-1):
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 # TODO: Investigate using the cluster name instead of X505_CERT_CN for
320 # common_name, as cluster-renames are very seldom, and it'd be nice if RAPI
321 # and node daemon certificates have the proper Subject/Issuer.
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
330 def GenerateSignedX509Cert(common_name, validity, serial_no,
331 signing_cert_pem):
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 # Create key pair with private and public key.
345 key_pair = OpenSSL.crypto.PKey()
346 key_pair.generate_key(OpenSSL.crypto.TYPE_RSA, constants.RSA_KEY_BITS)
347
348 # Create certificate sigining request.
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 # Load the certificates used for signing.
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 # Create a certificate and sign it.
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 # Encode the key and certificate in PEM format.
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
379 def GenerateSignedSslCert(filename_cert, serial_no,
380 filename_signing_cert,
381 common_name=constants.X509_CERT_CN,
382 validity=constants.X509_CERT_DEFAULT_VALIDITY,
383 uid=-1, gid=-1):
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)
390 return (key_pem, cert_pem)
391
392
393 def ExtractX509Certificate(pem):
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
407 def X509CertKeyCheck(cert, key):
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
425 def CheckNodeCertificate(cert, _noded_cert_file=pathutils.NODED_CERT_FILE):
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 # Check consistency of server.pem file
462 try:
463 X509CertKeyCheck(noded_cert, noded_key)
464 except OpenSSL.SSL.Error:
465 # This should never happen as it would mean the certificate in server.pem
466 # is out of sync with the private key stored in the same file
467 raise errors.X509CertError(_noded_cert_file,
468 "Certificate does not match with private key")
469
470 # Check with supplied certificate with local key
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")