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   
 58  # Certificate verification results 
 59  (CERT_WARNING, 
 60   CERT_ERROR) = range(1, 3) 
 61   
 62  #: ASN1 time regexp 
 63  _ASN1_TIME_REGEX = re.compile(r"^(\d+)([-+]\d\d)(\d\d)$") 
 64   
 65   
66 -def _ParseAsn1Generalizedtime(value):
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 # We have an offset 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
94 -def GetX509CertValidity(cert):
95 """Returns the validity period of the certificate. 96 97 @type cert: OpenSSL.crypto.X509 98 @param cert: X509 certificate object 99 100 """ 101 # The get_notBefore and get_notAfter functions are only supported in 102 # pyOpenSSL 0.7 and above. 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
130 -def _VerifyCertificateInner(expired, not_before, not_after, now, 131 warn_days, error_days):
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
181 -def VerifyX509Certificate(cert, warn_days, error_days):
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 # Depending on the pyOpenSSL version, this can just return (None, None) 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
201 -def SignX509Certificate(cert, key, salt):
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 # Dumping as PEM here ensures the certificate is in a sane format 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
228 -def _ExtractX509CertificateSignature(cert_pem):
229 """Helper function to extract signature from X509 certificate. 230 231 """ 232 # Extract signature from original PEM data 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
244 -def LoadSignedX509Certificate(cert_pem, key):
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 # Load and dump certificate to ensure it's in a sane format 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
266 -def GenerateSelfSignedX509Cert(common_name, validity, serial_no):
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 # Create private and public key 278 key = OpenSSL.crypto.PKey() 279 key.generate_key(OpenSSL.crypto.TYPE_RSA, constants.RSA_KEY_BITS) 280 281 # Create self-signed certificate 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
298 -def GenerateSelfSignedSslCert(filename, serial_no, 299 common_name=constants.X509_CERT_CN, 300 validity=constants.X509_CERT_DEFAULT_VALIDITY, 301 uid=-1, gid=-1):
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 # TODO: Investigate using the cluster name instead of X505_CERT_CN for 319 # common_name, as cluster-renames are very seldom, and it'd be nice if RAPI 320 # and node daemon certificates have the proper Subject/Issuer. 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
329 -def ExtractX509Certificate(pem):
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
343 -def PrepareX509CertKeyCheck(cert, key):
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
362 -def CheckNodeCertificate(cert, _noded_cert_file=pathutils.NODED_CERT_FILE):
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 # Check consistency of server.pem file 399 check_fn = PrepareX509CertKeyCheck(noded_cert, noded_key) 400 try: 401 check_fn() 402 except OpenSSL.SSL.Error: 403 # This should never happen as it would mean the certificate in server.pem 404 # is out of sync with the private key stored in the same file 405 raise errors.X509CertError(_noded_cert_file, 406 "Certificate does not match with private key") 407 408 # Check with supplied certificate with local key 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