mirror of
https://github.com/certbot/certbot.git
synced 2026-06-13 18:50:20 -04:00
Implement get_sans via parsing Request.as_text()
This commit is contained in:
parent
8ff99300e4
commit
36d26de746
4 changed files with 128 additions and 0 deletions
|
|
@ -1,4 +1,5 @@
|
|||
"""Let's Encrypt client crypto utility functions"""
|
||||
import re
|
||||
import time
|
||||
|
||||
import Crypto.Hash.SHA256
|
||||
|
|
@ -189,3 +190,43 @@ def get_cert_info(filename):
|
|||
"serial": cert.get_serial_number(),
|
||||
"pub_key": "RSA " + str(cert.get_pubkey().size() * 8),
|
||||
}
|
||||
|
||||
|
||||
def get_sans_from_csr(csr):
|
||||
"""Get list of Subject Alternative Names from signing request.
|
||||
|
||||
:param str csr: Certificate Signing Request in PEM format
|
||||
|
||||
:returns: List of referenced subject alternative names
|
||||
:rtype: list
|
||||
"""
|
||||
# TODO: This is a temporary solution involving parsing the .as_text()
|
||||
# output because there doesn't seem to be a built-in feature in
|
||||
# any Python cryptography module that performs this function.
|
||||
# In the future we should try to replace this with a more direct
|
||||
# use of relevant OpenSSL or other X509-parsing APIs.
|
||||
req = M2Crypto.X509.load_request_string(csr)
|
||||
text = req.as_text().split("\n")
|
||||
if len(text) < 2 or text[0] != "Certificate Request:" or \
|
||||
text[1] != " Data:":
|
||||
raise ValueError("Unable to parse CSR")
|
||||
text = text[2:]
|
||||
while text and text[0] != " Attributes:":
|
||||
text = text[1:]
|
||||
while text and text[0] != " Requested Extensions:":
|
||||
text = text[1:]
|
||||
while text and text[0] != " X509v3 Subject Alternative Name: ":
|
||||
text = text[1:]
|
||||
text = text[1:]
|
||||
if not text:
|
||||
raise ValueError("Unable to parse CSR")
|
||||
# XXX: This might break for non-ASCII hostnames and for non-DNS
|
||||
# names in SANs. There is also a parser safety concern about
|
||||
# whether the CSR's contents are interpreted in the same way
|
||||
# by this code and by any other code that might interpret the
|
||||
# CSR for a difference purpose.
|
||||
# All DNS names other than the last one
|
||||
matches = re.findall(r"(?:DNS:([\w.]+), )", text[0])
|
||||
# The last DNS name
|
||||
matches.append(re.search(r"(?:DNS:([\w.]+))$", text[0]).groups()[0])
|
||||
return matches
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Tests for letsencrypt.client.crypto_util."""
|
||||
import datetime
|
||||
import mock
|
||||
import os
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
|
@ -133,5 +134,71 @@ class GetCertInfoTest(unittest.TestCase):
|
|||
self._call('cert-san.pem')
|
||||
|
||||
|
||||
class GetSansFromCsrTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.crypto_util.get_sans_from_csr."""
|
||||
def test_extract_one_san(self):
|
||||
from letsencrypt.client.crypto_util import get_sans_from_csr
|
||||
csr = pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', 'csr.pem'))
|
||||
result = get_sans_from_csr(csr)
|
||||
self.assertEqual(result, ['example.com'])
|
||||
|
||||
def test_extract_two_sans(self):
|
||||
from letsencrypt.client.crypto_util import get_sans_from_csr
|
||||
csr = pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', 'csr-san.pem'))
|
||||
result = get_sans_from_csr(csr)
|
||||
self.assertEqual(result, ['example.com', 'www.example.com'])
|
||||
|
||||
def test_extract_six_sans(self):
|
||||
from letsencrypt.client.crypto_util import get_sans_from_csr
|
||||
csr = pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', 'csr-6sans.pem'))
|
||||
result = get_sans_from_csr(csr)
|
||||
self.assertEqual(
|
||||
result, ["example.com", "example.org", "example.net",
|
||||
"example.info", "subdomain.example.com",
|
||||
"other.subdomain.example.com"])
|
||||
|
||||
def test_parse_non_csr(self):
|
||||
from M2Crypto.X509 import X509Error
|
||||
from letsencrypt.client.crypto_util import get_sans_from_csr
|
||||
self.assertRaises(X509Error, get_sans_from_csr, "hello there")
|
||||
|
||||
def test_parse_no_sans(self):
|
||||
from letsencrypt.client.crypto_util import get_sans_from_csr
|
||||
csr = pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', 'csr-nosans.pem'))
|
||||
self.assertRaises(ValueError, get_sans_from_csr, csr)
|
||||
|
||||
@mock.patch("M2Crypto.X509.load_request_string")
|
||||
def test_parse_weird_m2crypto_output(self, mock_lrs):
|
||||
# It's not clear how to reach this exception with invalid input,
|
||||
# because M2Crypto is likely to raise X509Error rather than
|
||||
# returning invalid output, but we can test the possibility with
|
||||
# mock.
|
||||
mock_lrs.as_text.return_value = "Something other than OpenSSL output"
|
||||
from letsencrypt.client.crypto_util import get_sans_from_csr
|
||||
self.assertRaises(ValueError, get_sans_from_csr, "input")
|
||||
|
||||
class MakeCSRTest(unittest.TestCase): # pylint: disable=too-few-public-methods
|
||||
"""Tests for letsencrypt.client.crypto_util.make_csr."""
|
||||
def test_make_csr(self):
|
||||
from letsencrypt.client.crypto_util import make_csr, get_sans_from_csr
|
||||
result = make_csr(RSA512_KEY, ["example.com", "foo.example.com"])[0]
|
||||
self.assertEqual(
|
||||
get_sans_from_csr(result), ["example.com", "foo.example.com"])
|
||||
req = M2Crypto.X509.load_request_string(result)
|
||||
subject = req.get_subject().as_text()
|
||||
modulus = req.get_pubkey().get_modulus()
|
||||
self.assertEqual(
|
||||
subject, "C=US, ST=Michigan, L=Ann Arbor, O=EFF, OU=University"
|
||||
" of Michigan, CN=example.com")
|
||||
self.assertEqual(
|
||||
modulus, "F4B61171513736BFAA95E79C11C5FC2705439E3786D57EEE72C0"
|
||||
"9AB2EB993347B4F5C998B94CF12243233BFF71E0055CBD75D15CF"
|
||||
"115F8BCD65A47E44E5CD133")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
|||
12
letsencrypt/client/tests/testdata/csr-6sans.pem
vendored
Normal file
12
letsencrypt/client/tests/testdata/csr-6sans.pem
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw
|
||||
EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy
|
||||
c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG
|
||||
9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0
|
||||
9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG
|
||||
9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL
|
||||
ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t
|
||||
ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd
|
||||
k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv
|
||||
IvzVBz/nD11drfz/RNuX
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
8
letsencrypt/client/tests/testdata/csr-nosans.pem
vendored
Normal file
8
letsencrypt/client/tests/testdata/csr-nosans.pem
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
|
||||
MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt
|
||||
cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn
|
||||
BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz
|
||||
AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo
|
||||
wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA=
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
Loading…
Reference in a new issue