Revision 68857643

b/doc/design-2.2.rst
246 246
will be prepended to the certificate similar to an RFC822 header and
247 247
only covers the certificate (from ``-----BEGIN CERTIFICATE-----`` to
248 248
``-----END CERTIFICATE-----``). The header name will be
249
``X-Ganeti-Signature``.
249
``X-Ganeti-Signature`` and its value will have the format
250
``$salt/$hash`` (salt and hash separated by slash). The salt may only
251
contain characters in the range ``[a-zA-Z0-9]``.
250 252

  
251 253
On the web, the destination cluster would be equivalent to an HTTPS
252 254
server requiring verifiable client certificates. The browser would be
b/lib/constants.py
189 189
# Digest used to sign certificates ("openssl x509" uses SHA1 by default)
190 190
X509_CERT_SIGN_DIGEST = "SHA1"
191 191

  
192
X509_CERT_SIGNATURE_HEADER = "X-Ganeti-Signature"
193

  
192 194
VALUE_DEFAULT = "default"
193 195
VALUE_AUTO = "auto"
194 196
VALUE_GENERATE = "generate"
b/lib/utils.py
47 47
import OpenSSL
48 48
import datetime
49 49
import calendar
50
import hmac
50 51

  
51 52
from cStringIO import StringIO
52 53

  
53 54
try:
54 55
  from hashlib import sha1
55 56
except ImportError:
56
  import sha
57
  sha1 = sha.new
57
  import sha as sha1
58 58

  
59 59
from ganeti import errors
60 60
from ganeti import constants
......
70 70

  
71 71
_RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
72 72

  
73
HEX_CHAR_RE = r"[a-zA-Z0-9]"
74
VALID_X509_SIGNATURE_SALT = re.compile("^%s+$" % HEX_CHAR_RE, re.S)
75
X509_SIGNATURE = re.compile(r"^%s:\s*(?P<salt>%s+)/(?P<sign>%s+)$" %
76
                            (re.escape(constants.X509_CERT_SIGNATURE_HEADER),
77
                             HEX_CHAR_RE, HEX_CHAR_RE),
78
                            re.S | re.I)
79

  
73 80

  
74 81
class RunResult(object):
75 82
  """Holds the result of running external programs.
......
673 680

  
674 681
  f = open(filename)
675 682

  
676
  fp = sha1()
683
  if callable(sha1):
684
    fp = sha1()
685
  else:
686
    fp = sha1.new()
677 687
  while True:
678 688
    data = f.read(4096)
679 689
    if not data:
......
2356 2366
  return (not_before, not_after)
2357 2367

  
2358 2368

  
2369
def SignX509Certificate(cert, key, salt):
2370
  """Sign a X509 certificate.
2371

  
2372
  An RFC822-like signature header is added in front of the certificate.
2373

  
2374
  @type cert: OpenSSL.crypto.X509
2375
  @param cert: X509 certificate object
2376
  @type key: string
2377
  @param key: Key for HMAC
2378
  @type salt: string
2379
  @param salt: Salt for HMAC
2380
  @rtype: string
2381
  @return: Serialized and signed certificate in PEM format
2382

  
2383
  """
2384
  if not VALID_X509_SIGNATURE_SALT.match(salt):
2385
    raise errors.GenericError("Invalid salt: %r" % salt)
2386

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

  
2390
  return ("%s: %s/%s\n\n%s" %
2391
          (constants.X509_CERT_SIGNATURE_HEADER, salt,
2392
           hmac.new(key, salt + cert_pem, sha1).hexdigest(),
2393
           cert_pem))
2394

  
2395

  
2396
def _ExtractX509CertificateSignature(cert_pem):
2397
  """Helper function to extract signature from X509 certificate.
2398

  
2399
  """
2400
  # Extract signature from original PEM data
2401
  for line in cert_pem.splitlines():
2402
    if line.startswith("---"):
2403
      break
2404

  
2405
    m = X509_SIGNATURE.match(line.strip())
2406
    if m:
2407
      return (m.group("salt"), m.group("sign"))
2408

  
2409
  raise errors.GenericError("X509 certificate signature is missing")
2410

  
2411

  
2412
def LoadSignedX509Certificate(cert_pem, key):
2413
  """Verifies a signed X509 certificate.
2414

  
2415
  @type cert_pem: string
2416
  @param cert_pem: Certificate in PEM format and with signature header
2417
  @type key: string
2418
  @param key: Key for HMAC
2419
  @rtype: tuple; (OpenSSL.crypto.X509, string)
2420
  @return: X509 certificate object and salt
2421

  
2422
  """
2423
  (salt, signature) = _ExtractX509CertificateSignature(cert_pem)
2424

  
2425
  # Load certificate
2426
  cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
2427

  
2428
  # Dump again to ensure it's in a sane format
2429
  sane_pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
2430

  
2431
  if signature != hmac.new(key, salt + sane_pem, sha1).hexdigest():
2432
    raise errors.GenericError("X509 certificate signature is invalid")
2433

  
2434
  return (cert, salt)
2435

  
2436

  
2359 2437
def SafeEncode(text):
2360 2438
  """Return a 'safe' version of a source string.
2361 2439

  
b/test/ganeti.utils_unittest.py
1698 1698
      self.assertEqual(validity, (None, None))
1699 1699

  
1700 1700

  
1701
class TestSignX509Certificate(unittest.TestCase):
1702
  KEY = "My private key!"
1703
  KEY_OTHER = "Another key"
1704

  
1705
  def test(self):
1706
    # Generate certificate valid for 5 minutes
1707
    (_, cert_pem) = utils.GenerateSelfSignedX509Cert(None, 300)
1708

  
1709
    cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
1710
                                           cert_pem)
1711

  
1712
    # No signature at all
1713
    self.assertRaises(errors.GenericError,
1714
                      utils.LoadSignedX509Certificate, cert_pem, self.KEY)
1715

  
1716
    # Invalid input
1717
    self.assertRaises(errors.GenericError, utils.LoadSignedX509Certificate,
1718
                      "", self.KEY)
1719
    self.assertRaises(errors.GenericError, utils.LoadSignedX509Certificate,
1720
                      "X-Ganeti-Signature: \n", self.KEY)
1721
    self.assertRaises(errors.GenericError, utils.LoadSignedX509Certificate,
1722
                      "X-Ganeti-Sign: $1234$abcdef\n", self.KEY)
1723
    self.assertRaises(errors.GenericError, utils.LoadSignedX509Certificate,
1724
                      "X-Ganeti-Signature: $1234567890$abcdef\n", self.KEY)
1725
    self.assertRaises(errors.GenericError, utils.LoadSignedX509Certificate,
1726
                      "X-Ganeti-Signature: $1234$abc\n\n" + cert_pem, self.KEY)
1727

  
1728
    # Invalid salt
1729
    for salt in list("-_@$,:;/\\ \t\n"):
1730
      self.assertRaises(errors.GenericError, utils.SignX509Certificate,
1731
                        cert_pem, self.KEY, "foo%sbar" % salt)
1732

  
1733
    for salt in ["HelloWorld", "salt", string.letters, string.digits,
1734
                 utils.GenerateSecret(numbytes=4),
1735
                 utils.GenerateSecret(numbytes=16),
1736
                 "{123:456}".encode("hex")]:
1737
      signed_pem = utils.SignX509Certificate(cert, self.KEY, salt)
1738

  
1739
      self._Check(cert, salt, signed_pem)
1740

  
1741
      self._Check(cert, salt, "X-Another-Header: with a value\n" + signed_pem)
1742
      self._Check(cert, salt, (10 * "Hello World!\n") + signed_pem)
1743
      self._Check(cert, salt, (signed_pem + "\n\na few more\n"
1744
                               "lines----\n------ at\nthe end!"))
1745

  
1746
  def _Check(self, cert, salt, pem):
1747
    (cert2, salt2) = utils.LoadSignedX509Certificate(pem, self.KEY)
1748
    self.assertEqual(salt, salt2)
1749
    self.assertEqual(cert.digest("sha1"), cert2.digest("sha1"))
1750

  
1751
    # Other key
1752
    self.assertRaises(errors.GenericError, utils.LoadSignedX509Certificate,
1753
                      pem, self.KEY_OTHER)
1754

  
1755

  
1701 1756
if __name__ == '__main__':
1702 1757
  testutils.GanetiTestProgram()

Also available in: Unified diff