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):
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(1) 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, common_name=constants.X509_CERT_CN, 299 validity=constants.X509_CERT_DEFAULT_VALIDITY):
300 """Legacy function to generate self-signed X509 certificate. 301 302 @type filename: str 303 @param filename: path to write certificate to 304 @type common_name: string 305 @param common_name: commonName value 306 @type validity: int 307 @param validity: validity of certificate in number of days 308 @return: a tuple of strings containing the PEM-encoded private key and 309 certificate 310 311 """ 312 # TODO: Investigate using the cluster name instead of X505_CERT_CN for 313 # common_name, as cluster-renames are very seldom, and it'd be nice if RAPI 314 # and node daemon certificates have the proper Subject/Issuer. 315 (key_pem, cert_pem) = GenerateSelfSignedX509Cert(common_name, 316 validity * 24 * 60 * 60) 317 318 utils_io.WriteFile(filename, mode=0400, data=key_pem + cert_pem) 319 return (key_pem, cert_pem)
320 321
322 -def ExtractX509Certificate(pem):
323 """Extracts the certificate from a PEM-formatted string. 324 325 @type pem: string 326 @rtype: tuple; (OpenSSL.X509 object, string) 327 @return: Certificate object and PEM-formatted certificate 328 329 """ 330 cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem) 331 332 return (cert, 333 OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert))
334 335
336 -def PrepareX509CertKeyCheck(cert, key):
337 """Get function for verifying certificate with a certain private key. 338 339 @type key: OpenSSL.crypto.PKey 340 @param key: Private key object 341 @type cert: OpenSSL.crypto.X509 342 @param cert: X509 certificate object 343 @rtype: callable 344 @return: Callable doing the actual check; will raise C{OpenSSL.SSL.Error} if 345 certificate is not signed by given private key 346 347 """ 348 ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD) 349 ctx.use_privatekey(key) 350 ctx.use_certificate(cert) 351 352 return ctx.check_privatekey
353 354
355 -def CheckNodeCertificate(cert, _noded_cert_file=pathutils.NODED_CERT_FILE):
356 """Checks the local node daemon certificate against given certificate. 357 358 Both certificates must be signed with the same key (as stored in the local 359 L{pathutils.NODED_CERT_FILE} file). No error is raised if no local 360 certificate can be found. 361 362 @type cert: OpenSSL.crypto.X509 363 @param cert: X509 certificate object 364 @raise errors.X509CertError: When an error related to X509 occurred 365 @raise errors.GenericError: When the verification failed 366 367 """ 368 try: 369 noded_pem = utils_io.ReadFile(_noded_cert_file) 370 except EnvironmentError, err: 371 if err.errno != errno.ENOENT: 372 raise 373 374 logging.debug("Node certificate file '%s' was not found", _noded_cert_file) 375 return 376 377 try: 378 noded_cert = \ 379 OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, noded_pem) 380 except Exception, err: 381 raise errors.X509CertError(_noded_cert_file, 382 "Unable to load certificate: %s" % err) 383 384 try: 385 noded_key = \ 386 OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, noded_pem) 387 except Exception, err: 388 raise errors.X509CertError(_noded_cert_file, 389 "Unable to load private key: %s" % err) 390 391 # Check consistency of server.pem file 392 check_fn = PrepareX509CertKeyCheck(noded_cert, noded_key) 393 try: 394 check_fn() 395 except OpenSSL.SSL.Error: 396 # This should never happen as it would mean the certificate in server.pem 397 # is out of sync with the private key stored in the same file 398 raise errors.X509CertError(_noded_cert_file, 399 "Certificate does not match with private key") 400 401 # Check with supplied certificate with local key 402 check_fn = PrepareX509CertKeyCheck(cert, noded_key) 403 try: 404 check_fn() 405 except OpenSSL.SSL.Error: 406 raise errors.GenericError("Given cluster certificate does not match" 407 " local key")
408