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