Package ganeti :: Package utils :: Module x509
[hide private]
[frames] | no frames]

Source Code for Module ganeti.utils.x509

  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, backup=True) 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")
476