Revision c50645c0 lib/utils/__init__.py

b/lib/utils/__init__.py
43 43
import resource
44 44
import logging
45 45
import signal
46
import OpenSSL
47 46
import datetime
48 47
import calendar
49 48

  
......
62 61
from ganeti.utils.wrapper import * # pylint: disable-msg=W0401
63 62
from ganeti.utils.filelock import * # pylint: disable-msg=W0401
64 63
from ganeti.utils.io import * # pylint: disable-msg=W0401
64
from ganeti.utils.x509 import * # pylint: disable-msg=W0401
65 65

  
66 66

  
67 67
#: when set to True, L{RunCmd} is disabled
......
69 69

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

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

  
79 72
_VALID_SERVICE_NAME_RE = re.compile("^[-_.a-zA-Z0-9]{1,128}$")
80 73

  
81 74
UUID_RE = re.compile('^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-'
82 75
                     '[a-f0-9]{4}-[a-f0-9]{12}$')
83 76

  
84
# Certificate verification results
85
(CERT_WARNING,
86
 CERT_ERROR) = range(1, 3)
87

  
88 77
(_TIMEOUT_NONE,
89 78
 _TIMEOUT_TERM,
90 79
 _TIMEOUT_KILL) = range(3)
......
92 81
#: Shell param checker regexp
93 82
_SHELLPARAM_REGEX = re.compile(r"^[-a-zA-Z0-9._+/:%@]+$")
94 83

  
95
#: ASN1 time regexp
96
_ASN1_TIME_REGEX = re.compile(r"^(\d+)([-+]\d\d)(\d\d)$")
97

  
98 84

  
99 85
def DisableFork():
100 86
  """Disables the use of fork(2).
......
1572 1558
  return float(seconds) + (float(microseconds) * 0.000001)
1573 1559

  
1574 1560

  
1575
def _ParseAsn1Generalizedtime(value):
1576
  """Parses an ASN1 GENERALIZEDTIME timestamp as used by pyOpenSSL.
1577

  
1578
  @type value: string
1579
  @param value: ASN1 GENERALIZEDTIME timestamp
1580
  @return: Seconds since the Epoch (1970-01-01 00:00:00 UTC)
1581

  
1582
  """
1583
  m = _ASN1_TIME_REGEX.match(value)
1584
  if m:
1585
    # We have an offset
1586
    asn1time = m.group(1)
1587
    hours = int(m.group(2))
1588
    minutes = int(m.group(3))
1589
    utcoffset = (60 * hours) + minutes
1590
  else:
1591
    if not value.endswith("Z"):
1592
      raise ValueError("Missing timezone")
1593
    asn1time = value[:-1]
1594
    utcoffset = 0
1595

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

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

  
1600
  return calendar.timegm(tt.utctimetuple())
1601

  
1602

  
1603
def GetX509CertValidity(cert):
1604
  """Returns the validity period of the certificate.
1605

  
1606
  @type cert: OpenSSL.crypto.X509
1607
  @param cert: X509 certificate object
1608

  
1609
  """
1610
  # The get_notBefore and get_notAfter functions are only supported in
1611
  # pyOpenSSL 0.7 and above.
1612
  try:
1613
    get_notbefore_fn = cert.get_notBefore
1614
  except AttributeError:
1615
    not_before = None
1616
  else:
1617
    not_before_asn1 = get_notbefore_fn()
1618

  
1619
    if not_before_asn1 is None:
1620
      not_before = None
1621
    else:
1622
      not_before = _ParseAsn1Generalizedtime(not_before_asn1)
1623

  
1624
  try:
1625
    get_notafter_fn = cert.get_notAfter
1626
  except AttributeError:
1627
    not_after = None
1628
  else:
1629
    not_after_asn1 = get_notafter_fn()
1630

  
1631
    if not_after_asn1 is None:
1632
      not_after = None
1633
    else:
1634
      not_after = _ParseAsn1Generalizedtime(not_after_asn1)
1635

  
1636
  return (not_before, not_after)
1637

  
1638

  
1639
def _VerifyCertificateInner(expired, not_before, not_after, now,
1640
                            warn_days, error_days):
1641
  """Verifies certificate validity.
1642

  
1643
  @type expired: bool
1644
  @param expired: Whether pyOpenSSL considers the certificate as expired
1645
  @type not_before: number or None
1646
  @param not_before: Unix timestamp before which certificate is not valid
1647
  @type not_after: number or None
1648
  @param not_after: Unix timestamp after which certificate is invalid
1649
  @type now: number
1650
  @param now: Current time as Unix timestamp
1651
  @type warn_days: number or None
1652
  @param warn_days: How many days before expiration a warning should be reported
1653
  @type error_days: number or None
1654
  @param error_days: How many days before expiration an error should be reported
1655

  
1656
  """
1657
  if expired:
1658
    msg = "Certificate is expired"
1659

  
1660
    if not_before is not None and not_after is not None:
1661
      msg += (" (valid from %s to %s)" %
1662
              (FormatTime(not_before), FormatTime(not_after)))
1663
    elif not_before is not None:
1664
      msg += " (valid from %s)" % FormatTime(not_before)
1665
    elif not_after is not None:
1666
      msg += " (valid until %s)" % FormatTime(not_after)
1667

  
1668
    return (CERT_ERROR, msg)
1669

  
1670
  elif not_before is not None and not_before > now:
1671
    return (CERT_WARNING,
1672
            "Certificate not yet valid (valid from %s)" %
1673
            FormatTime(not_before))
1674

  
1675
  elif not_after is not None:
1676
    remaining_days = int((not_after - now) / (24 * 3600))
1677

  
1678
    msg = "Certificate expires in about %d days" % remaining_days
1679

  
1680
    if error_days is not None and remaining_days <= error_days:
1681
      return (CERT_ERROR, msg)
1682

  
1683
    if warn_days is not None and remaining_days <= warn_days:
1684
      return (CERT_WARNING, msg)
1685

  
1686
  return (None, None)
1687

  
1688

  
1689
def VerifyX509Certificate(cert, warn_days, error_days):
1690
  """Verifies a certificate for LUVerifyCluster.
1691

  
1692
  @type cert: OpenSSL.crypto.X509
1693
  @param cert: X509 certificate object
1694
  @type warn_days: number or None
1695
  @param warn_days: How many days before expiration a warning should be reported
1696
  @type error_days: number or None
1697
  @param error_days: How many days before expiration an error should be reported
1698

  
1699
  """
1700
  # Depending on the pyOpenSSL version, this can just return (None, None)
1701
  (not_before, not_after) = GetX509CertValidity(cert)
1702

  
1703
  return _VerifyCertificateInner(cert.has_expired(), not_before, not_after,
1704
                                 time.time(), warn_days, error_days)
1705

  
1706

  
1707
def SignX509Certificate(cert, key, salt):
1708
  """Sign a X509 certificate.
1709

  
1710
  An RFC822-like signature header is added in front of the certificate.
1711

  
1712
  @type cert: OpenSSL.crypto.X509
1713
  @param cert: X509 certificate object
1714
  @type key: string
1715
  @param key: Key for HMAC
1716
  @type salt: string
1717
  @param salt: Salt for HMAC
1718
  @rtype: string
1719
  @return: Serialized and signed certificate in PEM format
1720

  
1721
  """
1722
  if not VALID_X509_SIGNATURE_SALT.match(salt):
1723
    raise errors.GenericError("Invalid salt: %r" % salt)
1724

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

  
1728
  return ("%s: %s/%s\n\n%s" %
1729
          (constants.X509_CERT_SIGNATURE_HEADER, salt,
1730
           Sha1Hmac(key, cert_pem, salt=salt),
1731
           cert_pem))
1732

  
1733

  
1734
def _ExtractX509CertificateSignature(cert_pem):
1735
  """Helper function to extract signature from X509 certificate.
1736

  
1737
  """
1738
  # Extract signature from original PEM data
1739
  for line in cert_pem.splitlines():
1740
    if line.startswith("---"):
1741
      break
1742

  
1743
    m = X509_SIGNATURE.match(line.strip())
1744
    if m:
1745
      return (m.group("salt"), m.group("sign"))
1746

  
1747
  raise errors.GenericError("X509 certificate signature is missing")
1748

  
1749

  
1750
def LoadSignedX509Certificate(cert_pem, key):
1751
  """Verifies a signed X509 certificate.
1752

  
1753
  @type cert_pem: string
1754
  @param cert_pem: Certificate in PEM format and with signature header
1755
  @type key: string
1756
  @param key: Key for HMAC
1757
  @rtype: tuple; (OpenSSL.crypto.X509, string)
1758
  @return: X509 certificate object and salt
1759

  
1760
  """
1761
  (salt, signature) = _ExtractX509CertificateSignature(cert_pem)
1762

  
1763
  # Load certificate
1764
  cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
1765

  
1766
  # Dump again to ensure it's in a sane format
1767
  sane_pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
1768

  
1769
  if not VerifySha1Hmac(key, sane_pem, signature, salt=salt):
1770
    raise errors.GenericError("X509 certificate signature is invalid")
1771

  
1772
  return (cert, salt)
1773

  
1774

  
1775 1561
def FindMatch(data, name):
1776 1562
  """Tries to find an item in a dictionary matching a name.
1777 1563

  
......
1866 1652
  return bool(exitcode)
1867 1653

  
1868 1654

  
1869
def GenerateSelfSignedX509Cert(common_name, validity):
1870
  """Generates a self-signed X509 certificate.
1871

  
1872
  @type common_name: string
1873
  @param common_name: commonName value
1874
  @type validity: int
1875
  @param validity: Validity for certificate in seconds
1876

  
1877
  """
1878
  # Create private and public key
1879
  key = OpenSSL.crypto.PKey()
1880
  key.generate_key(OpenSSL.crypto.TYPE_RSA, constants.RSA_KEY_BITS)
1881

  
1882
  # Create self-signed certificate
1883
  cert = OpenSSL.crypto.X509()
1884
  if common_name:
1885
    cert.get_subject().CN = common_name
1886
  cert.set_serial_number(1)
1887
  cert.gmtime_adj_notBefore(0)
1888
  cert.gmtime_adj_notAfter(validity)
1889
  cert.set_issuer(cert.get_subject())
1890
  cert.set_pubkey(key)
1891
  cert.sign(key, constants.X509_CERT_SIGN_DIGEST)
1892

  
1893
  key_pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
1894
  cert_pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
1895

  
1896
  return (key_pem, cert_pem)
1897

  
1898

  
1899
def GenerateSelfSignedSslCert(filename, common_name=constants.X509_CERT_CN,
1900
                              validity=constants.X509_CERT_DEFAULT_VALIDITY):
1901
  """Legacy function to generate self-signed X509 certificate.
1902

  
1903
  @type filename: str
1904
  @param filename: path to write certificate to
1905
  @type common_name: string
1906
  @param common_name: commonName value
1907
  @type validity: int
1908
  @param validity: validity of certificate in number of days
1909

  
1910
  """
1911
  # TODO: Investigate using the cluster name instead of X505_CERT_CN for
1912
  # common_name, as cluster-renames are very seldom, and it'd be nice if RAPI
1913
  # and node daemon certificates have the proper Subject/Issuer.
1914
  (key_pem, cert_pem) = GenerateSelfSignedX509Cert(common_name,
1915
                                                   validity * 24 * 60 * 60)
1916

  
1917
  WriteFile(filename, mode=0400, data=key_pem + cert_pem)
1918

  
1919

  
1920 1655
def SignalHandled(signums):
1921 1656
  """Signal Handled decoration.
1922 1657

  

Also available in: Unified diff