Merge pull request #1853 from reaperhulk/certificate-policies

certificate policies extension support
diff --git a/docs/x509.rst b/docs/x509.rst
index 86673e3..d231329 100644
--- a/docs/x509.rst
+++ b/docs/x509.rst
@@ -892,6 +892,81 @@
         removed from the CRL. This reason cannot be used as a reason flag
         in a :class:`DistributionPoint`.
 
+.. class:: CertificatePolicies
+
+    .. versionadded:: 0.9
+
+    The certificate policies extension is an iterable, containing one or more
+    :class:`PolicyInformation` instances.
+
+Certificate Policies Classes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+These classes may be present within a :class:`CertificatePolicies` instance.
+
+.. class:: PolicyInformation
+
+    .. versionadded:: 0.9
+
+    Contains a policy identifier and an optional list of qualifiers.
+
+    .. attribute:: policy_identifier
+
+        :type: :class:`ObjectIdentifier`
+
+    .. attribute:: policy_qualifiers
+
+        :type: list
+
+        A list consisting of :term:`text` and/or :class:`UserNotice` objects.
+        If the value is text it is a pointer to the practice statement
+        published by the certificate authority. If it is a user notice it is
+        meant for display to the relying party when the certificate is
+        used.
+
+.. class:: UserNotice
+
+    .. versionadded:: 0.9
+
+    User notices are intended for display to a relying party when a certificate
+    is used. In practice, few if any UIs expose this data and it is a rarely
+    encoded component.
+
+    .. attribute:: notice_reference
+
+        :type: :class:`NoticeReference` or None
+
+        The notice reference field names an organization and identifies,
+        by number, a particular statement prepared by that organization.
+
+    .. attribute:: explicit_text
+
+        This field includes an arbitrary textual statement directly in the
+        certificate.
+
+        :type: :term:`text`
+
+.. class:: NoticeReference
+
+    Notice reference can name an organization and provide information about
+    notices related to the certificate. For example, it might identify the
+    organization name and notice number 1. Application software could
+    have a notice file containing the current set of notices for the named
+    organization; the application would then extract the notice text from the
+    file and display it. In practice this is rarely seen.
+
+    .. versionadded:: 0.9
+
+    .. attribute:: organization
+
+        :type: :term:`text` or None
+
+    .. attribute:: notice_numbers
+
+        :type: list or None
+
+        A list of integers or None.
+
 Object Identifiers
 ~~~~~~~~~~~~~~~~~~
 
@@ -1097,6 +1172,17 @@
     Corresponds to the dotted string ``"1.3.6.1.5.5.7.48.2"``. Used as the
     identifier for CA issuer data in :class:`AccessDescription` objects.
 
+Policy Qualifier OIDs
+~~~~~~~~~~~~~~~~~~~~~
+
+.. data:: OID_CPS_QUALIFIER
+
+    Corresponds to the dotted string ``"1.3.6.1.5.5.7.2.1"``.
+
+.. data:: OID_CPS_USER_NOTICE
+
+    Corresponds to the dotted string ``"1.3.6.1.5.5.7.2.2"``.
+
 .. _extension_oids:
 
 Extension OIDs
diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py
index dfc0af8..50fae71 100644
--- a/src/cryptography/x509.py
+++ b/src/cryptography/x509.py
@@ -69,6 +69,8 @@
     "1.3.6.1.5.5.7.48.1.5": "OCSPNoCheck",
     "1.3.6.1.5.5.7.48.1": "OCSP",
     "1.3.6.1.5.5.7.48.2": "caIssuers",
+    "1.3.6.1.5.5.7.2.1": "id-qt-cps",
+    "1.3.6.1.5.5.7.2.2": "id-qt-unotice",
 }
 
 
@@ -460,6 +462,98 @@
     access_location = utils.read_only_property("_access_location")
 
 
+class CertificatePolicies(object):
+    def __init__(self, policies):
+        if not all(isinstance(x, PolicyInformation) for x in policies):
+            raise TypeError(
+                "Every item in the policies list must be a "
+                "PolicyInformation"
+            )
+
+        self._policies = policies
+
+    def __iter__(self):
+        return iter(self._policies)
+
+    def __len__(self):
+        return len(self._policies)
+
+    def __repr__(self):
+        return "<CertificatePolicies({0})>".format(self._policies)
+
+
+class PolicyInformation(object):
+    def __init__(self, policy_identifier, policy_qualifiers):
+        if not isinstance(policy_identifier, ObjectIdentifier):
+            raise TypeError("policy_identifier must be an ObjectIdentifier")
+
+        self._policy_identifier = policy_identifier
+        if policy_qualifiers and not all(
+            isinstance(
+                x, (six.text_type, UserNotice)
+            ) for x in policy_qualifiers
+        ):
+            raise TypeError(
+                "policy_qualifiers must be a list of strings and/or UserNotice"
+                " objects or None"
+            )
+
+        self._policy_qualifiers = policy_qualifiers
+
+    def __repr__(self):
+        return (
+            "<PolicyInformation(policy_identifier={0.policy_identifier}, polic"
+            "y_qualifiers={0.policy_qualifiers})>".format(self)
+        )
+
+    policy_identifier = utils.read_only_property("_policy_identifier")
+    policy_qualifiers = utils.read_only_property("_policy_qualifiers")
+
+
+class UserNotice(object):
+    def __init__(self, notice_reference, explicit_text):
+        if notice_reference and not isinstance(
+            notice_reference, NoticeReference
+        ):
+            raise TypeError(
+                "notice_reference must be None or a NoticeReference"
+            )
+
+        self._notice_reference = notice_reference
+        self._explicit_text = explicit_text
+
+    def __repr__(self):
+        return (
+            "<UserNotice(notice_reference={0.notice_reference}, explicit_text="
+            "{0.explicit_text!r})>".format(self)
+        )
+
+    notice_reference = utils.read_only_property("_notice_reference")
+    explicit_text = utils.read_only_property("_explicit_text")
+
+
+class NoticeReference(object):
+    def __init__(self, organization, notice_numbers):
+        self._organization = organization
+        if notice_numbers and not all(
+            isinstance(x, int) for x in notice_numbers
+        ):
+            raise TypeError(
+                "notice_numbers must be a list of integers or None"
+            )
+
+        self._notice_numbers = notice_numbers
+
+    def __repr__(self):
+        return (
+            "<NoticeReference(organization={0.organization!r}, notice_numbers="
+            "{0.notice_numbers})>".format(self)
+        )
+
+    organization = utils.read_only_property("_organization")
+    notice_numbers = utils.read_only_property("_notice_numbers")
+
+
 class SubjectKeyIdentifier(object):
     def __init__(self, digest):
         self._digest = digest
@@ -874,6 +968,9 @@
 OID_CA_ISSUERS = ObjectIdentifier("1.3.6.1.5.5.7.48.2")
 OID_OCSP = ObjectIdentifier("1.3.6.1.5.5.7.48.1")
 
+OID_CPS_QUALIFIER = ObjectIdentifier("1.3.6.1.5.5.7.2.1")
+OID_CPS_USER_NOTICE = ObjectIdentifier("1.3.6.1.5.5.7.2.2")
+
 
 @six.add_metaclass(abc.ABCMeta)
 class Certificate(object):
diff --git a/tests/test_x509_ext.py b/tests/test_x509_ext.py
index 06a6860..ae69f5f 100644
--- a/tests/test_x509_ext.py
+++ b/tests/test_x509_ext.py
@@ -39,6 +39,125 @@
         )
 
 
+class TestNoticeReference(object):
+    def test_notice_numbers_not_all_int(self):
+        with pytest.raises(TypeError):
+            x509.NoticeReference("org", [1, 2, "three"])
+
+    def test_notice_numbers_none(self):
+        nr = x509.NoticeReference("org", None)
+        assert nr.organization == "org"
+        assert nr.notice_numbers is None
+
+    def test_repr(self):
+        nr = x509.NoticeReference(u"org", [1, 3, 4])
+
+        if six.PY3:
+            assert repr(nr) == (
+                "<NoticeReference(organization='org', notice_numbers=[1, 3, 4"
+                "])>"
+            )
+        else:
+            assert repr(nr) == (
+                "<NoticeReference(organization=u'org', notice_numbers=[1, 3, "
+                "4])>"
+            )
+
+
+class TestUserNotice(object):
+    def test_notice_reference_invalid(self):
+        with pytest.raises(TypeError):
+            x509.UserNotice("invalid", None)
+
+    def test_notice_reference_none(self):
+        un = x509.UserNotice(None, "text")
+        assert un.notice_reference is None
+        assert un.explicit_text == "text"
+
+    def test_repr(self):
+        un = x509.UserNotice(x509.NoticeReference(u"org", None), u"text")
+        if six.PY3:
+            assert repr(un) == (
+                "<UserNotice(notice_reference=<NoticeReference(organization='"
+                "org', notice_numbers=None)>, explicit_text='text')>"
+            )
+        else:
+            assert repr(un) == (
+                "<UserNotice(notice_reference=<NoticeReference(organization=u"
+                "'org', notice_numbers=None)>, explicit_text=u'text')>"
+            )
+
+
+class TestPolicyInformation(object):
+    def test_invalid_policy_identifier(self):
+        with pytest.raises(TypeError):
+            x509.PolicyInformation("notanoid", None)
+
+    def test_none_policy_qualifiers(self):
+        pi = x509.PolicyInformation(x509.ObjectIdentifier("1.2.3"), None)
+        assert pi.policy_identifier == x509.ObjectIdentifier("1.2.3")
+        assert pi.policy_qualifiers is None
+
+    def test_policy_qualifiers(self):
+        pq = [u"string"]
+        pi = x509.PolicyInformation(x509.ObjectIdentifier("1.2.3"), pq)
+        assert pi.policy_identifier == x509.ObjectIdentifier("1.2.3")
+        assert pi.policy_qualifiers == pq
+
+    def test_invalid_policy_identifiers(self):
+        with pytest.raises(TypeError):
+            x509.PolicyInformation(x509.ObjectIdentifier("1.2.3"), [1, 2])
+
+    def test_repr(self):
+        pq = [u"string", x509.UserNotice(None, u"hi")]
+        pi = x509.PolicyInformation(x509.ObjectIdentifier("1.2.3"), pq)
+        if six.PY3:
+            assert repr(pi) == (
+                "<PolicyInformation(policy_identifier=<ObjectIdentifier(oid=1."
+                "2.3, name=Unknown OID)>, policy_qualifiers=['string', <UserNo"
+                "tice(notice_reference=None, explicit_text='hi')>])>"
+            )
+        else:
+            assert repr(pi) == (
+                "<PolicyInformation(policy_identifier=<ObjectIdentifier(oid=1."
+                "2.3, name=Unknown OID)>, policy_qualifiers=[u'string', <UserN"
+                "otice(notice_reference=None, explicit_text=u'hi')>])>"
+            )
+
+
+class TestCertificatePolicies(object):
+    def test_invalid_policies(self):
+        pq = [u"string"]
+        pi = x509.PolicyInformation(x509.ObjectIdentifier("1.2.3"), pq)
+        with pytest.raises(TypeError):
+            x509.CertificatePolicies([1, pi])
+
+    def test_iter_len(self):
+        pq = [u"string"]
+        pi = x509.PolicyInformation(x509.ObjectIdentifier("1.2.3"), pq)
+        cp = x509.CertificatePolicies([pi])
+        assert len(cp) == 1
+        for policyinfo in cp:
+            assert policyinfo == pi
+
+    def test_repr(self):
+        pq = [u"string"]
+        pi = x509.PolicyInformation(x509.ObjectIdentifier("1.2.3"), pq)
+        cp = x509.CertificatePolicies([pi])
+        if six.PY3:
+            assert repr(cp) == (
+                "<CertificatePolicies([<PolicyInformation(policy_identifier=<O"
+                "bjectIdentifier(oid=1.2.3, name=Unknown OID)>, policy_qualifi"
+                "ers=['string'])>])>"
+            )
+        else:
+            assert repr(cp) == (
+                "<CertificatePolicies([<PolicyInformation(policy_identifier=<O"
+                "bjectIdentifier(oid=1.2.3, name=Unknown OID)>, policy_qualifi"
+                "ers=[u'string'])>])>"
+            )
+
+
 class TestKeyUsage(object):
     def test_key_agreement_false_encipher_decipher_true(self):
         with pytest.raises(ValueError):