Statistics
| Branch: | Tag: | Revision:

root / lib / utils / x509.py @ e1a6abf9

History | View | Annotate | Download (12.3 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010, 2011, 2012 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21
"""Utility functions for X509.
22

23
"""
24

    
25
import time
26
import OpenSSL
27
import re
28
import datetime
29
import calendar
30
import errno
31
import logging
32

    
33
from ganeti import errors
34
from ganeti import constants
35
from ganeti import pathutils
36

    
37
from ganeti.utils import text as utils_text
38
from ganeti.utils import io as utils_io
39
from ganeti.utils import hash as utils_hash
40

    
41

    
42
HEX_CHAR_RE = r"[a-zA-Z0-9]"
43
VALID_X509_SIGNATURE_SALT = re.compile("^%s+$" % HEX_CHAR_RE, re.S)
44
X509_SIGNATURE = re.compile(r"^%s:\s*(?P<salt>%s+)/(?P<sign>%s+)$" %
45
                            (re.escape(constants.X509_CERT_SIGNATURE_HEADER),
46
                             HEX_CHAR_RE, HEX_CHAR_RE),
47
                            re.S | re.I)
48

    
49
# Certificate verification results
50
(CERT_WARNING,
51
 CERT_ERROR) = range(1, 3)
52

    
53
#: ASN1 time regexp
54
_ASN1_TIME_REGEX = re.compile(r"^(\d+)([-+]\d\d)(\d\d)$")
55

    
56

    
57
def _ParseAsn1Generalizedtime(value):
58
  """Parses an ASN1 GENERALIZEDTIME timestamp as used by pyOpenSSL.
59

60
  @type value: string
61
  @param value: ASN1 GENERALIZEDTIME timestamp
62
  @return: Seconds since the Epoch (1970-01-01 00:00:00 UTC)
63

64
  """
65
  m = _ASN1_TIME_REGEX.match(value)
66
  if m:
67
    # We have an offset
68
    asn1time = m.group(1)
69
    hours = int(m.group(2))
70
    minutes = int(m.group(3))
71
    utcoffset = (60 * hours) + minutes
72
  else:
73
    if not value.endswith("Z"):
74
      raise ValueError("Missing timezone")
75
    asn1time = value[:-1]
76
    utcoffset = 0
77

    
78
  parsed = time.strptime(asn1time, "%Y%m%d%H%M%S")
79

    
80
  tt = datetime.datetime(*(parsed[:7])) - datetime.timedelta(minutes=utcoffset)
81

    
82
  return calendar.timegm(tt.utctimetuple())
83

    
84

    
85
def GetX509CertValidity(cert):
86
  """Returns the validity period of the certificate.
87

88
  @type cert: OpenSSL.crypto.X509
89
  @param cert: X509 certificate object
90

91
  """
92
  # The get_notBefore and get_notAfter functions are only supported in
93
  # pyOpenSSL 0.7 and above.
94
  try:
95
    get_notbefore_fn = cert.get_notBefore
96
  except AttributeError:
97
    not_before = None
98
  else:
99
    not_before_asn1 = get_notbefore_fn()
100

    
101
    if not_before_asn1 is None:
102
      not_before = None
103
    else:
104
      not_before = _ParseAsn1Generalizedtime(not_before_asn1)
105

    
106
  try:
107
    get_notafter_fn = cert.get_notAfter
108
  except AttributeError:
109
    not_after = None
110
  else:
111
    not_after_asn1 = get_notafter_fn()
112

    
113
    if not_after_asn1 is None:
114
      not_after = None
115
    else:
116
      not_after = _ParseAsn1Generalizedtime(not_after_asn1)
117

    
118
  return (not_before, not_after)
119

    
120

    
121
def _VerifyCertificateInner(expired, not_before, not_after, now,
122
                            warn_days, error_days):
123
  """Verifies certificate validity.
124

125
  @type expired: bool
126
  @param expired: Whether pyOpenSSL considers the certificate as expired
127
  @type not_before: number or None
128
  @param not_before: Unix timestamp before which certificate is not valid
129
  @type not_after: number or None
130
  @param not_after: Unix timestamp after which certificate is invalid
131
  @type now: number
132
  @param now: Current time as Unix timestamp
133
  @type warn_days: number or None
134
  @param warn_days: How many days before expiration a warning should be reported
135
  @type error_days: number or None
136
  @param error_days: How many days before expiration an error should be reported
137

138
  """
139
  if expired:
140
    msg = "Certificate is expired"
141

    
142
    if not_before is not None and not_after is not None:
143
      msg += (" (valid from %s to %s)" %
144
              (utils_text.FormatTime(not_before),
145
               utils_text.FormatTime(not_after)))
146
    elif not_before is not None:
147
      msg += " (valid from %s)" % utils_text.FormatTime(not_before)
148
    elif not_after is not None:
149
      msg += " (valid until %s)" % utils_text.FormatTime(not_after)
150

    
151
    return (CERT_ERROR, msg)
152

    
153
  elif not_before is not None and not_before > now:
154
    return (CERT_WARNING,
155
            "Certificate not yet valid (valid from %s)" %
156
            utils_text.FormatTime(not_before))
157

    
158
  elif not_after is not None:
159
    remaining_days = int((not_after - now) / (24 * 3600))
160

    
161
    msg = "Certificate expires in about %d days" % remaining_days
162

    
163
    if error_days is not None and remaining_days <= error_days:
164
      return (CERT_ERROR, msg)
165

    
166
    if warn_days is not None and remaining_days <= warn_days:
167
      return (CERT_WARNING, msg)
168

    
169
  return (None, None)
170

    
171

    
172
def VerifyX509Certificate(cert, warn_days, error_days):
173
  """Verifies a certificate for LUClusterVerify.
174

175
  @type cert: OpenSSL.crypto.X509
176
  @param cert: X509 certificate object
177
  @type warn_days: number or None
178
  @param warn_days: How many days before expiration a warning should be reported
179
  @type error_days: number or None
180
  @param error_days: How many days before expiration an error should be reported
181

182
  """
183
  # Depending on the pyOpenSSL version, this can just return (None, None)
184
  (not_before, not_after) = GetX509CertValidity(cert)
185

    
186
  now = time.time() + constants.NODE_MAX_CLOCK_SKEW
187

    
188
  return _VerifyCertificateInner(cert.has_expired(), not_before, not_after,
189
                                 now, warn_days, error_days)
190

    
191

    
192
def SignX509Certificate(cert, key, salt):
193
  """Sign a X509 certificate.
194

195
  An RFC822-like signature header is added in front of the certificate.
196

197
  @type cert: OpenSSL.crypto.X509
198
  @param cert: X509 certificate object
199
  @type key: string
200
  @param key: Key for HMAC
201
  @type salt: string
202
  @param salt: Salt for HMAC
203
  @rtype: string
204
  @return: Serialized and signed certificate in PEM format
205

206
  """
207
  if not VALID_X509_SIGNATURE_SALT.match(salt):
208
    raise errors.GenericError("Invalid salt: %r" % salt)
209

    
210
  # Dumping as PEM here ensures the certificate is in a sane format
211
  cert_pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
212

    
213
  return ("%s: %s/%s\n\n%s" %
214
          (constants.X509_CERT_SIGNATURE_HEADER, salt,
215
           utils_hash.Sha1Hmac(key, cert_pem, salt=salt),
216
           cert_pem))
217

    
218

    
219
def _ExtractX509CertificateSignature(cert_pem):
220
  """Helper function to extract signature from X509 certificate.
221

222
  """
223
  # Extract signature from original PEM data
224
  for line in cert_pem.splitlines():
225
    if line.startswith("---"):
226
      break
227

    
228
    m = X509_SIGNATURE.match(line.strip())
229
    if m:
230
      return (m.group("salt"), m.group("sign"))
231

    
232
  raise errors.GenericError("X509 certificate signature is missing")
233

    
234

    
235
def LoadSignedX509Certificate(cert_pem, key):
236
  """Verifies a signed X509 certificate.
237

238
  @type cert_pem: string
239
  @param cert_pem: Certificate in PEM format and with signature header
240
  @type key: string
241
  @param key: Key for HMAC
242
  @rtype: tuple; (OpenSSL.crypto.X509, string)
243
  @return: X509 certificate object and salt
244

245
  """
246
  (salt, signature) = _ExtractX509CertificateSignature(cert_pem)
247

    
248
  # Load and dump certificate to ensure it's in a sane format
249
  (cert, sane_pem) = ExtractX509Certificate(cert_pem)
250

    
251
  if not utils_hash.VerifySha1Hmac(key, sane_pem, signature, salt=salt):
252
    raise errors.GenericError("X509 certificate signature is invalid")
253

    
254
  return (cert, salt)
255

    
256

    
257
def GenerateSelfSignedX509Cert(common_name, validity):
258
  """Generates a self-signed X509 certificate.
259

260
  @type common_name: string
261
  @param common_name: commonName value
262
  @type validity: int
263
  @param validity: Validity for certificate in seconds
264
  @return: a tuple of strings containing the PEM-encoded private key and
265
           certificate
266

267
  """
268
  # Create private and public key
269
  key = OpenSSL.crypto.PKey()
270
  key.generate_key(OpenSSL.crypto.TYPE_RSA, constants.RSA_KEY_BITS)
271

    
272
  # Create self-signed certificate
273
  cert = OpenSSL.crypto.X509()
274
  if common_name:
275
    cert.get_subject().CN = common_name
276
  cert.set_serial_number(1)
277
  cert.gmtime_adj_notBefore(0)
278
  cert.gmtime_adj_notAfter(validity)
279
  cert.set_issuer(cert.get_subject())
280
  cert.set_pubkey(key)
281
  cert.sign(key, constants.X509_CERT_SIGN_DIGEST)
282

    
283
  key_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
284
  cert_pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
285

    
286
  return (key_pem, cert_pem)
287

    
288

    
289
def GenerateSelfSignedSslCert(filename, common_name=constants.X509_CERT_CN,
290
                              validity=constants.X509_CERT_DEFAULT_VALIDITY):
291
  """Legacy function to generate self-signed X509 certificate.
292

293
  @type filename: str
294
  @param filename: path to write certificate to
295
  @type common_name: string
296
  @param common_name: commonName value
297
  @type validity: int
298
  @param validity: validity of certificate in number of days
299
  @return: a tuple of strings containing the PEM-encoded private key and
300
           certificate
301

302
  """
303
  # TODO: Investigate using the cluster name instead of X505_CERT_CN for
304
  # common_name, as cluster-renames are very seldom, and it'd be nice if RAPI
305
  # and node daemon certificates have the proper Subject/Issuer.
306
  (key_pem, cert_pem) = GenerateSelfSignedX509Cert(common_name,
307
                                                   validity * 24 * 60 * 60)
308

    
309
  utils_io.WriteFile(filename, mode=0400, data=key_pem + cert_pem)
310
  return (key_pem, cert_pem)
311

    
312

    
313
def ExtractX509Certificate(pem):
314
  """Extracts the certificate from a PEM-formatted string.
315

316
  @type pem: string
317
  @rtype: tuple; (OpenSSL.X509 object, string)
318
  @return: Certificate object and PEM-formatted certificate
319

320
  """
321
  cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem)
322

    
323
  return (cert,
324
          OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert))
325

    
326

    
327
def PrepareX509CertKeyCheck(cert, key):
328
  """Get function for verifying certificate with a certain private key.
329

330
  @type key: OpenSSL.crypto.PKey
331
  @param key: Private key object
332
  @type cert: OpenSSL.crypto.X509
333
  @param cert: X509 certificate object
334
  @rtype: callable
335
  @return: Callable doing the actual check; will raise C{OpenSSL.SSL.Error} if
336
    certificate is not signed by given private key
337

338
  """
339
  ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
340
  ctx.use_privatekey(key)
341
  ctx.use_certificate(cert)
342

    
343
  return ctx.check_privatekey
344

    
345

    
346
def CheckNodeCertificate(cert, _noded_cert_file=pathutils.NODED_CERT_FILE):
347
  """Checks the local node daemon certificate against given certificate.
348

349
  Both certificates must be signed with the same key (as stored in the local
350
  L{pathutils.NODED_CERT_FILE} file). No error is raised if no local
351
  certificate can be found.
352

353
  @type cert: OpenSSL.crypto.X509
354
  @param cert: X509 certificate object
355
  @raise errors.X509CertError: When an error related to X509 occurred
356
  @raise errors.GenericError: When the verification failed
357

358
  """
359
  try:
360
    noded_pem = utils_io.ReadFile(_noded_cert_file)
361
  except EnvironmentError, err:
362
    if err.errno != errno.ENOENT:
363
      raise
364

    
365
    logging.debug("Node certificate file '%s' was not found", _noded_cert_file)
366
    return
367

    
368
  try:
369
    noded_cert = \
370
      OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, noded_pem)
371
  except Exception, err:
372
    raise errors.X509CertError(_noded_cert_file,
373
                               "Unable to load certificate: %s" % err)
374

    
375
  try:
376
    noded_key = \
377
      OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, noded_pem)
378
  except Exception, err:
379
    raise errors.X509CertError(_noded_cert_file,
380
                               "Unable to load private key: %s" % err)
381

    
382
  # Check consistency of server.pem file
383
  check_fn = PrepareX509CertKeyCheck(noded_cert, noded_key)
384
  try:
385
    check_fn()
386
  except OpenSSL.SSL.Error:
387
    # This should never happen as it would mean the certificate in server.pem
388
    # is out of sync with the private key stored in the same file
389
    raise errors.X509CertError(_noded_cert_file,
390
                               "Certificate does not match with private key")
391

    
392
  # Check with supplied certificate with local key
393
  check_fn = PrepareX509CertKeyCheck(cert, noded_key)
394
  try:
395
    check_fn()
396
  except OpenSSL.SSL.Error:
397
    raise errors.GenericError("Given cluster certificate does not match"
398
                              " local key")