From d0d29ccc324bb9f95bffbe3162ee5c3c61c6086a Mon Sep 17 00:00:00 2001 From: Fraser Tweedale Date: Jul 18 2019 07:01:25 +0000 Subject: move MSCSTemplate classes to ipalib As we expand the integration tests for external CA functionality, it is helpful (and avoids duplication) to use the MSCSTemplate* classes. These currently live in ipaserver.install.cainstance, but ipatests is no longer permitted to import from ipaserver (see commit 81714976e5e13131654c78eb734746a20237c933). So move these classes to ipalib. Part of: https://pagure.io/freeipa/issue/7548 Reviewed-By: Florence Blanc-Renaud --- diff --git a/install/tools/ipa-ca-install.in b/install/tools/ipa-ca-install.in index 0700c0c..ce6d5fc 100644 --- a/install/tools/ipa-ca-install.in +++ b/install/tools/ipa-ca-install.in @@ -37,7 +37,7 @@ from ipaserver.install import cainstance, service from ipaserver.install import custodiainstance from ipaserver.masters import find_providing_server from ipapython import version -from ipalib import api +from ipalib import api, x509 from ipalib.constants import DOMAIN_LEVEL_1 from ipapython.config import IPAOptionParser from ipapython.ipa_log_manager import standard_logging_setup @@ -68,13 +68,13 @@ def parse_options(): default=False, help="unattended installation never prompts the user") parser.add_option("--external-ca", dest="external_ca", action="store_true", default=False, help="Generate a CSR to be signed by an external CA") - ext_cas = tuple(x.value for x in cainstance.ExternalCAType) + ext_cas = tuple(x.value for x in x509.ExternalCAType) parser.add_option("--external-ca-type", dest="external_ca_type", type="choice", choices=ext_cas, metavar="{{{0}}}".format(",".join(ext_cas)), help="Type of the external CA. Default: generic") parser.add_option("--external-ca-profile", dest="external_ca_profile", - type='constructor', constructor=cainstance.ExternalCAProfile, + type='constructor', constructor=x509.ExternalCAProfile, default=None, metavar="PROFILE-SPEC", help="Specify the certificate profile/template to use " "at the external CA") diff --git a/ipalib/x509.py b/ipalib/x509.py index ab3c5f5..1f612a3 100644 --- a/ipalib/x509.py +++ b/ipalib/x509.py @@ -34,6 +34,7 @@ from __future__ import print_function import os import binascii import datetime +import enum import ipaddress import ssl import base64 @@ -47,6 +48,7 @@ from cryptography.hazmat.primitives.serialization import ( Encoding, PublicFormat, PrivateFormat, load_pem_private_key ) import pyasn1 +import pyasn1.error from pyasn1.type import univ, char, namedtype, tag from pyasn1.codec.der import decoder, encoder from pyasn1_modules import rfc2315, rfc2459 @@ -745,3 +747,172 @@ def format_datetime(t): if t.tzinfo is None: t = t.replace(tzinfo=UTC()) return unicode(t.strftime("%a %b %d %H:%M:%S %Y %Z")) + + +class ExternalCAType(enum.Enum): + GENERIC = 'generic' + MS_CS = 'ms-cs' + + +class ExternalCAProfile: + """ + An external CA profile configuration. Currently the only + subclasses are for Microsoft CAs, for providing data in the + "Certificate Template" extension. + + Constructing this class will actually return an instance of a + subclass. + + Subclasses MUST set ``valid_for``. + + """ + def __init__(self, s=None): + self.unparsed_input = s + + # Which external CA types is the data valid for? + # A set of VALUES of the ExternalCAType enum. + valid_for = set() + + def __new__(cls, s=None): + """Construct the ExternalCAProfile value. + + Return an instance of a subclass determined by + the format of the argument. + + """ + # we are directly constructing a subclass; instantiate + # it and be done + if cls is not ExternalCAProfile: + return super(ExternalCAProfile, cls).__new__(cls) + + # construction via the base class; therefore the string + # argument is required, and is used to determine which + # subclass to construct + if s is None: + raise ValueError('string argument is required') + + parts = s.split(':') + + try: + # Is the first part on OID? + _oid = univ.ObjectIdentifier(parts[0]) + + # It is; construct a V2 template + # pylint: disable=too-many-function-args + return MSCSTemplateV2.__new__(MSCSTemplateV2, s) + + except pyasn1.error.PyAsn1Error: + # It is not an OID; treat as a template name + # pylint: disable=too-many-function-args + return MSCSTemplateV1.__new__(MSCSTemplateV1, s) + + def __getstate__(self): + return self.unparsed_input + + def __setstate__(self, state): + # explicitly call __init__ method to initialise object + self.__init__(state) + + +class MSCSTemplate(ExternalCAProfile): + """ + An Microsoft AD-CS Template specifier. + + Subclasses MUST set ext_oid. + + Subclass constructors MUST set asn1obj. + + """ + valid_for = set([ExternalCAType.MS_CS.value]) + + ext_oid = None # extension OID, as a Python str + asn1obj = None # unencoded extension data + + def get_ext_data(self): + """Return DER-encoded extension data.""" + return encoder.encode(self.asn1obj) + + +class MSCSTemplateV1(MSCSTemplate): + """ + A v1 template specifier, per + https://msdn.microsoft.com/en-us/library/cc250011.aspx. + + :: + + CertificateTemplateName ::= SEQUENCE { + Name UTF8String + } + + But note that a bare BMPString is used in practice. + + """ + ext_oid = "1.3.6.1.4.1.311.20.2" + + def __init__(self, s): + super(MSCSTemplateV1, self).__init__(s) + parts = s.split(':') + if len(parts) > 1: + raise ValueError( + "Cannot specify certificate template version when using name.") + self.asn1obj = char.BMPString(str(parts[0])) + + +class MSCSTemplateV2(MSCSTemplate): + """ + A v2 template specifier, per + https://msdn.microsoft.com/en-us/library/windows/desktop/aa378274(v=vs.85).aspx + + :: + + CertificateTemplate ::= SEQUENCE { + templateID EncodedObjectID, + templateMajorVersion TemplateVersion, + templateMinorVersion TemplateVersion OPTIONAL + } + + TemplateVersion ::= INTEGER (0..4294967295) + + """ + ext_oid = "1.3.6.1.4.1.311.21.7" + + @staticmethod + def check_version_in_range(desc, n): + if n < 0 or n >= 2**32: + raise ValueError( + "Template {} version must be in range 0..4294967295" + .format(desc)) + + def __init__(self, s): + super(MSCSTemplateV2, self).__init__(s) + + parts = s.split(':') + + obj = CertificateTemplateV2() + if len(parts) < 2 or len(parts) > 3: + raise ValueError( + "Incorrect template specification; required format is: " + ":[:]") + try: + obj['templateID'] = univ.ObjectIdentifier(parts[0]) + + major = int(parts[1]) + self.check_version_in_range("major", major) + obj['templateMajorVersion'] = major + + if len(parts) > 2: + minor = int(parts[2]) + self.check_version_in_range("minor", minor) + obj['templateMinorVersion'] = int(parts[2]) + + except pyasn1.error.PyAsn1Error: + raise ValueError("Could not parse certificate template specifier.") + self.asn1obj = obj + + +class CertificateTemplateV2(univ.Sequence): + componentType = namedtype.NamedTypes( + namedtype.NamedType('templateID', univ.ObjectIdentifier()), + namedtype.NamedType('templateMajorVersion', univ.Integer()), + namedtype.OptionalNamedType('templateMinorVersion', univ.Integer()) + ) diff --git a/ipaserver/install/ca.py b/ipaserver/install/ca.py index 6b040b3..8fb5e3e 100644 --- a/ipaserver/install/ca.py +++ b/ipaserver/install/ca.py @@ -28,7 +28,7 @@ from ipaplatform import services from ipaplatform.paths import paths from ipaserver.install import installutils, certs from ipaserver.install.replication import replica_conn_check -from ipalib import api, errors +from ipalib import api, errors, x509 from ipapython.dn import DN from . import conncheck, dogtag, cainstance @@ -216,8 +216,7 @@ def install_check(standalone, replica_config, options): paths.ROOT_IPA_CSR) if not options.external_ca_type: - options.external_ca_type = \ - cainstance.ExternalCAType.GENERIC.value + options.external_ca_type = x509.ExternalCAType.GENERIC.value if options.external_ca_profile is not None: # check that profile is valid for the external ca type @@ -478,13 +477,11 @@ class CAInstallInterface(dogtag.DogtagInstallInterface, external_ca = master_install_only(external_ca) external_ca_type = knob( - cainstance.ExternalCAType, None, - description="Type of the external CA", - ) + x509.ExternalCAType, None, description="Type of the external CA") external_ca_type = master_install_only(external_ca_type) external_ca_profile = knob( - type=cainstance.ExternalCAProfile, + type=x509.ExternalCAProfile, default=None, description=( "Specify the certificate profile/template to use at the " diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py index 6e1fc72..2295581 100644 --- a/ipaserver/install/cainstance.py +++ b/ipaserver/install/cainstance.py @@ -26,7 +26,6 @@ import binascii import logging import dbus -import enum import ldap import os import pwd @@ -39,10 +38,6 @@ import time import tempfile from configparser import RawConfigParser -from pyasn1.codec.der import encoder -from pyasn1.type import char, univ, namedtype -import pyasn1.error - from ipalib import api from ipalib import x509 from ipalib import errors @@ -80,11 +75,6 @@ ADMIN_GROUPS = [ ] -class ExternalCAType(enum.Enum): - GENERIC = 'generic' - MS_CS = 'ms-cs' - - def check_ports(): """Check that dogtag ports (8080, 8443) are available. @@ -367,7 +357,7 @@ class CAInstance(DogtagInstance): if ca_type is not None: self.ca_type = ca_type else: - self.ca_type = ExternalCAType.GENERIC.value + self.ca_type = x509.ExternalCAType.GENERIC.value self.external_ca_profile = external_ca_profile self.no_db_setup = promote @@ -537,12 +527,12 @@ class CAInstance(DogtagInstance): pki_ca_signing_csr_path=self.csr_file, ) - if self.ca_type == ExternalCAType.MS_CS.value: + if self.ca_type == x509.ExternalCAType.MS_CS.value: # Include MS template name extension in the CSR template = self.external_ca_profile if template is None: # default template name - template = MSCSTemplateV1(u"SubCA") + template = x509.MSCSTemplateV1(u"SubCA") ext_data = binascii.hexlify(template.get_ext_data()) cfg.update( @@ -2081,170 +2071,6 @@ def update_ipa_conf(): parser.write(f) -class ExternalCAProfile: - """ - An external CA profile configuration. Currently the only - subclasses are for Microsoft CAs, for providing data in the - "Certificate Template" extension. - - Constructing this class will actually return an instance of a - subclass. - - Subclasses MUST set ``valid_for``. - - """ - def __init__(self, s=None): - self.unparsed_input = s - - # Which external CA types is the data valid for? - # A set of VALUES of the ExternalCAType enum. - valid_for = set() - - def __new__(cls, s=None): - """Construct the ExternalCAProfile value. - - Return an instance of a subclass determined by - the format of the argument. - - """ - # we are directly constructing a subclass; instantiate - # it and be done - if cls is not ExternalCAProfile: - return super(ExternalCAProfile, cls).__new__(cls) - - # construction via the base class; therefore the string - # argument is required, and is used to determine which - # subclass to construct - if s is None: - raise ValueError('string argument is required') - - parts = s.split(':') - - try: - # Is the first part on OID? - _oid = univ.ObjectIdentifier(parts[0]) - - # It is; construct a V2 template - # pylint: disable=too-many-function-args - return MSCSTemplateV2.__new__(MSCSTemplateV2, s) - - except pyasn1.error.PyAsn1Error: - # It is not an OID; treat as a template name - # pylint: disable=too-many-function-args - return MSCSTemplateV1.__new__(MSCSTemplateV1, s) - - def __getstate__(self): - return self.unparsed_input - - def __setstate__(self, state): - # explicitly call __init__ method to initialise object - self.__init__(state) - - -class MSCSTemplate(ExternalCAProfile): - """ - An Microsoft AD-CS Template specifier. - - Subclasses MUST set ext_oid. - - Subclass constructors MUST set asn1obj. - - """ - valid_for = set([ExternalCAType.MS_CS.value]) - - ext_oid = None # extension OID, as a Python str - asn1obj = None # unencoded extension data - - def get_ext_data(self): - """Return DER-encoded extension data.""" - return encoder.encode(self.asn1obj) - - -class MSCSTemplateV1(MSCSTemplate): - """ - A v1 template specifier, per - https://msdn.microsoft.com/en-us/library/cc250011.aspx. - - :: - - CertificateTemplateName ::= SEQUENCE { - Name UTF8String - } - - But note that a bare BMPString is used in practice. - - """ - ext_oid = "1.3.6.1.4.1.311.20.2" - - def __init__(self, s): - super(MSCSTemplateV1, self).__init__(s) - parts = s.split(':') - if len(parts) > 1: - raise ValueError( - "Cannot specify certificate template version when using name.") - self.asn1obj = char.BMPString(str(parts[0])) - - -class MSCSTemplateV2(MSCSTemplate): - """ - A v2 template specifier, per - https://msdn.microsoft.com/en-us/library/windows/desktop/aa378274(v=vs.85).aspx - - :: - - CertificateTemplate ::= SEQUENCE { - templateID EncodedObjectID, - templateMajorVersion TemplateVersion, - templateMinorVersion TemplateVersion OPTIONAL - } - - TemplateVersion ::= INTEGER (0..4294967295) - - """ - ext_oid = "1.3.6.1.4.1.311.21.7" - - @staticmethod - def check_version_in_range(desc, n): - if n < 0 or n >= 2**32: - raise ValueError( - "Template {} version must be in range 0..4294967295" - .format(desc)) - - def __init__(self, s): - super(MSCSTemplateV2, self).__init__(s) - - parts = s.split(':') - - obj = CertificateTemplateV2() - if len(parts) < 2 or len(parts) > 3: - raise ValueError( - "Incorrect template specification; required format is: " - ":[:]") - try: - obj['templateID'] = univ.ObjectIdentifier(parts[0]) - - major = int(parts[1]) - self.check_version_in_range("major", major) - obj['templateMajorVersion'] = major - - if len(parts) > 2: - minor = int(parts[2]) - self.check_version_in_range("minor", minor) - obj['templateMinorVersion'] = int(parts[2]) - - except pyasn1.error.PyAsn1Error: - raise ValueError("Could not parse certificate template specifier.") - self.asn1obj = obj - - -class CertificateTemplateV2(univ.Sequence): - componentType = namedtype.NamedTypes( - namedtype.NamedType('templateID', univ.ObjectIdentifier()), - namedtype.NamedType('templateMajorVersion', univ.Integer()), - namedtype.OptionalNamedType('templateMinorVersion', univ.Integer()) - ) - - if __name__ == "__main__": standard_logging_setup("install.log") ds = dsinstance.DsInstance() diff --git a/ipaserver/install/ipa_cacert_manage.py b/ipaserver/install/ipa_cacert_manage.py index 3f113c3..37dcc2b 100644 --- a/ipaserver/install/ipa_cacert_manage.py +++ b/ipaserver/install/ipa_cacert_manage.py @@ -65,7 +65,7 @@ class CACertManage(admintool.AdminTool): "--external-ca", dest='self_signed', action='store_false', help="Sign the renewed certificate by external CA") - ext_cas = tuple(x.value for x in cainstance.ExternalCAType) + ext_cas = tuple(x.value for x in x509.ExternalCAType) renew_group.add_option( "--external-ca-type", dest="external_ca_type", type="choice", choices=ext_cas, @@ -73,7 +73,7 @@ class CACertManage(admintool.AdminTool): help="Type of the external CA. Default: generic") renew_group.add_option( "--external-ca-profile", dest="external_ca_profile", - type='constructor', constructor=cainstance.ExternalCAProfile, + type='constructor', constructor=x509.ExternalCAProfile, default=None, metavar="PROFILE-SPEC", help="Specify the certificate profile/template to use " "at the external CA") @@ -224,11 +224,11 @@ class CACertManage(admintool.AdminTool): options = self.options if not options.external_ca_type: - options.external_ca_type = cainstance.ExternalCAType.GENERIC.value + options.external_ca_type = x509.ExternalCAType.GENERIC.value - if options.external_ca_type == cainstance.ExternalCAType.MS_CS.value \ + if options.external_ca_type == x509.ExternalCAType.MS_CS.value \ and options.external_ca_profile is None: - options.external_ca_profile = cainstance.MSCSTemplateV1(u"SubCA") + options.external_ca_profile = x509.MSCSTemplateV1(u"SubCA") if options.external_ca_profile is not None: # check that profile is valid for the external ca type @@ -352,11 +352,11 @@ class CACertManage(admintool.AdminTool): timeout = api.env.startup_timeout + 60 cm_profile = None - if isinstance(profile, cainstance.MSCSTemplateV1): + if isinstance(profile, x509.MSCSTemplateV1): cm_profile = profile.unparsed_input cm_template = None - if isinstance(profile, cainstance.MSCSTemplateV2): + if isinstance(profile, x509.MSCSTemplateV2): cm_template = profile.unparsed_input logger.debug("resubmitting certmonger request '%s'", self.request_id) diff --git a/ipatests/test_integration/test_external_ca.py b/ipatests/test_integration/test_external_ca.py index a423552..5aa2b7b 100644 --- a/ipatests/test_integration/test_external_ca.py +++ b/ipatests/test_integration/test_external_ca.py @@ -108,14 +108,14 @@ def check_ipaca_issuerDN(host, expected_dn): assert "Issuer DN: {}".format(expected_dn) in result.stdout_text -def check_mscs_extension(ipa_csr, oid, value): +def check_mscs_extension(ipa_csr, template): csr = x509.load_pem_x509_csr(ipa_csr, default_backend()) extensions = [ ext for ext in csr.extensions - if ext.oid.dotted_string == oid + if ext.oid.dotted_string == template.ext_oid ] assert extensions - assert extensions[0].value.value == value + assert extensions[0].value.value == template.get_ext_data() class TestExternalCA(IntegrationTest): @@ -134,10 +134,7 @@ class TestExternalCA(IntegrationTest): # check CSR for extension ipa_csr = self.master.get_file_contents(paths.ROOT_IPA_CSR) - # Values for MSCSTemplateV1('SubCA') - oid = "1.3.6.1.4.1.311.20.2" - value = b'\x1e\n\x00S\x00u\x00b\x00C\x00A' - check_mscs_extension(ipa_csr, oid, value) + check_mscs_extension(ipa_csr, ipa_x509.MSCSTemplateV1(u'SubCA')) # Sign CA, transport it to the host and get ipa a root ca paths. root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport( diff --git a/ipatests/test_ipalib/test_x509.py b/ipatests/test_ipalib/test_x509.py index ff7e6de..284b998 100644 --- a/ipatests/test_ipalib/test_x509.py +++ b/ipatests/test_ipalib/test_x509.py @@ -22,7 +22,11 @@ Test the `ipalib.x509` module. """ import base64 +from binascii import hexlify +from configparser import RawConfigParser import datetime +from io import StringIO +import pickle import pytest @@ -268,3 +272,114 @@ class test_x509: b'0 \x06\x03U\x1d%\x01\x01\xff\x04\x160\x14\x06\x08+\x06\x01' b'\x05\x05\x07\x03\x01\x06\x08+\x06\x01\x05\x05\x07\x03\x02' ) + + +class test_ExternalCAProfile: + def test_MSCSTemplateV1_good(self): + o = x509.MSCSTemplateV1("MySubCA") + assert hexlify(o.get_ext_data()) == b'1e0e004d007900530075006200430041' + + def test_MSCSTemplateV1_bad(self): + with pytest.raises(ValueError): + x509.MSCSTemplateV1("MySubCA:1") + + def test_MSCSTemplateV1_pickle_roundtrip(self): + o = x509.MSCSTemplateV1("MySubCA") + s = pickle.dumps(o) + assert o.get_ext_data() == pickle.loads(s).get_ext_data() + + def test_MSCSTemplateV2_too_few_parts(self): + with pytest.raises(ValueError): + x509.MSCSTemplateV2("1.2.3.4") + + def test_MSCSTemplateV2_too_many_parts(self): + with pytest.raises(ValueError): + x509.MSCSTemplateV2("1.2.3.4:100:200:300") + + def test_MSCSTemplateV2_bad_oid(self): + with pytest.raises(ValueError): + x509.MSCSTemplateV2("not_an_oid:1") + + def test_MSCSTemplateV2_non_numeric_major_version(self): + with pytest.raises(ValueError): + x509.MSCSTemplateV2("1.2.3.4:major:200") + + def test_MSCSTemplateV2_non_numeric_minor_version(self): + with pytest.raises(ValueError): + x509.MSCSTemplateV2("1.2.3.4:100:minor") + + def test_MSCSTemplateV2_major_version_lt_zero(self): + with pytest.raises(ValueError): + x509.MSCSTemplateV2("1.2.3.4:-1:200") + + def test_MSCSTemplateV2_minor_version_lt_zero(self): + with pytest.raises(ValueError): + x509.MSCSTemplateV2("1.2.3.4:100:-1") + + def test_MSCSTemplateV2_major_version_gt_max(self): + with pytest.raises(ValueError): + x509.MSCSTemplateV2("1.2.3.4:4294967296:200") + + def test_MSCSTemplateV2_minor_version_gt_max(self): + with pytest.raises(ValueError): + x509.MSCSTemplateV2("1.2.3.4:100:4294967296") + + def test_MSCSTemplateV2_good_major(self): + o = x509.MSCSTemplateV2("1.2.3.4:4294967295") + assert hexlify(o.get_ext_data()) == b'300c06032a0304020500ffffffff' + + def test_MSCSTemplateV2_good_major_minor(self): + o = x509.MSCSTemplateV2("1.2.3.4:4294967295:0") + assert hexlify(o.get_ext_data()) \ + == b'300f06032a0304020500ffffffff020100' + + def test_MSCSTemplateV2_pickle_roundtrip(self): + o = x509.MSCSTemplateV2("1.2.3.4:4294967295:0") + s = pickle.dumps(o) + assert o.get_ext_data() == pickle.loads(s).get_ext_data() + + def test_ExternalCAProfile_dispatch(self): + """ + Test that constructing ExternalCAProfile actually returns an + instance of the appropriate subclass. + """ + assert isinstance( + x509.ExternalCAProfile("MySubCA"), + x509.MSCSTemplateV1) + assert isinstance( + x509.ExternalCAProfile("1.2.3.4:100"), + x509.MSCSTemplateV2) + + def test_write_pkispawn_config_file_MSCSTemplateV1(self): + template = x509.MSCSTemplateV1(u"SubCA") + expected = ( + '[CA]\n' + 'pki_req_ext_oid = 1.3.6.1.4.1.311.20.2\n' + 'pki_req_ext_data = 1e0a00530075006200430041\n\n' + ) + self._test_write_pkispawn_config_file(template, expected) + + def test_write_pkispawn_config_file_MSCSTemplateV2(self): + template = x509.MSCSTemplateV2(u"1.2.3.4:4294967295") + expected = ( + '[CA]\n' + 'pki_req_ext_oid = 1.3.6.1.4.1.311.21.7\n' + 'pki_req_ext_data = 300c06032a0304020500ffffffff\n\n' + ) + self._test_write_pkispawn_config_file(template, expected) + + def _test_write_pkispawn_config_file(self, template, expected): + """ + Test that the values we read from an ExternalCAProfile + object can be used to produce a reasonable-looking pkispawn + configuration. + """ + config = RawConfigParser() + config.optionxform = str + config.add_section("CA") + config.set("CA", "pki_req_ext_oid", template.ext_oid) + config.set("CA", "pki_req_ext_data", + hexlify(template.get_ext_data()).decode('ascii')) + out = StringIO() + config.write(out) + assert out.getvalue() == expected diff --git a/ipatests/test_ipaserver/test_install/test_cainstance.py b/ipatests/test_ipaserver/test_install/test_cainstance.py deleted file mode 100644 index 02d9758..0000000 --- a/ipatests/test_ipaserver/test_install/test_cainstance.py +++ /dev/null @@ -1,123 +0,0 @@ -# -# Copyright (C) 2017 FreeIPA Contributors see COPYING for license -# - -from binascii import hexlify -from io import StringIO -import pickle -from configparser import RawConfigParser -import pytest -from ipaserver.install import cainstance - -pytestmark = pytest.mark.tier0 - - -class test_ExternalCAProfile: - def test_MSCSTemplateV1_good(self): - o = cainstance.MSCSTemplateV1("MySubCA") - assert hexlify(o.get_ext_data()) == b'1e0e004d007900530075006200430041' - - def test_MSCSTemplateV1_bad(self): - with pytest.raises(ValueError): - cainstance.MSCSTemplateV1("MySubCA:1") - - def test_MSCSTemplateV1_pickle_roundtrip(self): - o = cainstance.MSCSTemplateV1("MySubCA") - s = pickle.dumps(o) - assert o.get_ext_data() == pickle.loads(s).get_ext_data() - - def test_MSCSTemplateV2_too_few_parts(self): - with pytest.raises(ValueError): - cainstance.MSCSTemplateV2("1.2.3.4") - - def test_MSCSTemplateV2_too_many_parts(self): - with pytest.raises(ValueError): - cainstance.MSCSTemplateV2("1.2.3.4:100:200:300") - - def test_MSCSTemplateV2_bad_oid(self): - with pytest.raises(ValueError): - cainstance.MSCSTemplateV2("not_an_oid:1") - - def test_MSCSTemplateV2_non_numeric_major_version(self): - with pytest.raises(ValueError): - cainstance.MSCSTemplateV2("1.2.3.4:major:200") - - def test_MSCSTemplateV2_non_numeric_minor_version(self): - with pytest.raises(ValueError): - cainstance.MSCSTemplateV2("1.2.3.4:100:minor") - - def test_MSCSTemplateV2_major_version_lt_zero(self): - with pytest.raises(ValueError): - cainstance.MSCSTemplateV2("1.2.3.4:-1:200") - - def test_MSCSTemplateV2_minor_version_lt_zero(self): - with pytest.raises(ValueError): - cainstance.MSCSTemplateV2("1.2.3.4:100:-1") - - def test_MSCSTemplateV2_major_version_gt_max(self): - with pytest.raises(ValueError): - cainstance.MSCSTemplateV2("1.2.3.4:4294967296:200") - - def test_MSCSTemplateV2_minor_version_gt_max(self): - with pytest.raises(ValueError): - cainstance.MSCSTemplateV2("1.2.3.4:100:4294967296") - - def test_MSCSTemplateV2_good_major(self): - o = cainstance.MSCSTemplateV2("1.2.3.4:4294967295") - assert hexlify(o.get_ext_data()) == b'300c06032a0304020500ffffffff' - - def test_MSCSTemplateV2_good_major_minor(self): - o = cainstance.MSCSTemplateV2("1.2.3.4:4294967295:0") - assert hexlify(o.get_ext_data()) \ - == b'300f06032a0304020500ffffffff020100' - - def test_MSCSTemplateV2_pickle_roundtrip(self): - o = cainstance.MSCSTemplateV2("1.2.3.4:4294967295:0") - s = pickle.dumps(o) - assert o.get_ext_data() == pickle.loads(s).get_ext_data() - - def test_ExternalCAProfile_dispatch(self): - """ - Test that constructing ExternalCAProfile actually returns an - instance of the appropriate subclass. - """ - assert isinstance( - cainstance.ExternalCAProfile("MySubCA"), - cainstance.MSCSTemplateV1) - assert isinstance( - cainstance.ExternalCAProfile("1.2.3.4:100"), - cainstance.MSCSTemplateV2) - - def test_write_pkispawn_config_file_MSCSTemplateV1(self): - template = cainstance.MSCSTemplateV1(u"SubCA") - expected = ( - '[CA]\n' - 'pki_req_ext_oid = 1.3.6.1.4.1.311.20.2\n' - 'pki_req_ext_data = 1e0a00530075006200430041\n\n' - ) - self._test_write_pkispawn_config_file(template, expected) - - def test_write_pkispawn_config_file_MSCSTemplateV2(self): - template = cainstance.MSCSTemplateV2(u"1.2.3.4:4294967295") - expected = ( - '[CA]\n' - 'pki_req_ext_oid = 1.3.6.1.4.1.311.21.7\n' - 'pki_req_ext_data = 300c06032a0304020500ffffffff\n\n' - ) - self._test_write_pkispawn_config_file(template, expected) - - def _test_write_pkispawn_config_file(self, template, expected): - """ - Test that the values we read from an ExternalCAProfile - object can be used to produce a reasonable-looking pkispawn - configuration. - """ - config = RawConfigParser() - config.optionxform = str - config.add_section("CA") - config.set("CA", "pki_req_ext_oid", template.ext_oid) - config.set("CA", "pki_req_ext_data", - hexlify(template.get_ext_data()).decode('ascii')) - out = StringIO() - config.write(out) - assert out.getvalue() == expected