Repository: privacyidea/privacyidea

Issue:
email address in CSR causes request to fail
Adding an email to the subject results in a denied CSR with the MSCA

https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/tokens/certificatetoken.py#L472

So we need to define, if the CSR should contain the email address. This might be related to #2159 

In #2159 we propose a policy scope `ca`.

We could add a policy:

* scope: ca
* action: request_subject_components = email

However, this could be dependent on the template type.

Also look into localCA to check the additional value here

As a condition for such a policy, we would also need:

* Name of the CA issued to
* template name issued to.

So this would be #3364.

However, I think in a first step it is fine to create a simple policy - either in scope CA or in scope enrollment.

* [x] documentation
* [x] implement decorator `policy ca_request_subject_component = email` in scope enrollment 
* [x] test
* [x] Implement adding of components to request
* [x] test



Discussion:

Resolving patch:
diff --git a/privacyidea/api/lib/prepolicy.py b/privacyidea/api/lib/prepolicy.py
--- a/privacyidea/api/lib/prepolicy.py
+++ b/privacyidea/api/lib/prepolicy.py
@@ -647,6 +647,28 @@ def init_ca_template(request=None, action=None):
             request.all_data["template"] = list(template_pols)[0]
 
 
+def init_subject_components(request=None, action=None):
+    """
+    This is a pre decorator for the endpoint '/token/init'.
+    It reads the policy scope=enrollment, action=certificate_request_subject_component and
+    sets the API parameter "subject_component" accordingly.
+
+    :param request: The request to enhance
+    :return: None, but we modify the request
+    """
+    params = request.all_data
+    user_object = get_user_from_param(params)
+    token_type = getParam(request.all_data, "type", optional)
+    if token_type and token_type.lower() == "certificate":
+        # get the subject list from the policies
+        subject_pols = Match.user(g, scope=SCOPE.ENROLL,
+                                  action=CERTIFICATE_ACTION.CERTIFICATE_REQUEST_SUBJECT_COMPONENT,
+                                  user_object=user_object).action_values(unique=False)
+        if len(list(subject_pols)):
+            # The policy was set, we need to add the list to the parameters
+            request.all_data["subject_components"] = list(subject_pols)
+
+
 def twostep_enrollment_activation(request=None, action=None):
     """
     This policy function enables the two-step enrollment process according
diff --git a/privacyidea/api/token.py b/privacyidea/api/token.py
--- a/privacyidea/api/token.py
+++ b/privacyidea/api/token.py
@@ -101,7 +101,8 @@
                                            indexedsecret_force_attribute,
                                            check_admin_tokenlist, webauthntoken_enroll, webauthntoken_allowed,
                                            webauthntoken_request, required_piv_attestation,
-                                           hide_tokeninfo, init_ca_connector, init_ca_template)
+                                           hide_tokeninfo, init_ca_connector, init_ca_template,
+                                           init_subject_components)
 from privacyidea.api.lib.postpolicy import (save_pin_change, check_verify_enrollment,
                                             postpolicy)
 from privacyidea.lib.event import event
@@ -131,6 +132,7 @@
 @prepolicy(init_tokenlabel, request)
 @prepolicy(init_ca_connector, request)
 @prepolicy(init_ca_template, request)
+@prepolicy(init_subject_components, request)
 @prepolicy(enroll_pin, request)
 @prepolicy(twostep_enrollment_activation, request)
 @prepolicy(twostep_enrollment_parameters, request)
diff --git a/privacyidea/lib/tokens/certificatetoken.py b/privacyidea/lib/tokens/certificatetoken.py
--- a/privacyidea/lib/tokens/certificatetoken.py
+++ b/privacyidea/lib/tokens/certificatetoken.py
@@ -69,6 +69,7 @@ class ACTION(BASE_ACTION):
     REQUIRE_ATTESTATION = "certificate_require_attestation"
     CA_CONNECTOR = "ca_connector"
     CERTIFICATE_TEMPLATE = "certificate_template"
+    CERTIFICATE_REQUEST_SUBJECT_COMPONENT = "certificate_request_subject_component"
 
 
 class REQUIRE_ACTIONS(object):
@@ -316,6 +317,12 @@ def get_class_info(key=None, ret='all'):
                            'type': 'str',
                            'desc': _("The template that should be used to issue a certificate."),
                            'group': GROUP.TOKEN
+                       },
+                       ACTION.CERTIFICATE_REQUEST_SUBJECT_COMPONENT: {
+                           'type': 'str',
+                           'desc': _("This takes a space separated list of elements to be added to the subject. "
+                                     "Can be 'email' and 'realm'."),
+                           'group': GROUP.TOKEN
                        }
                    },
                    SCOPE.USER: {
@@ -424,6 +431,7 @@ def update(self, param):
         certificate = getParam(param, "certificate", optional)
         generate = getParam(param, "genkey", optional)
         template_name = getParam(param, "template", optional)
+        subject_components = getParam(param, "subject_components", optional=optional, default=[])
         request_id = None
         if request or generate:
             # If we do not upload a user certificate, then we need a CA do
@@ -461,32 +469,33 @@ def update(self, param):
                         if verify_attestation:
                             raise privacyIDEAError("Failed to verify certificate chain of attestation certificate.")
 
-            # During the initialization process, we need to create the
-            # certificate
+            # During the initialization process, we need to create the certificate
             request_id, x509object = cacon.sign_request(request,
                                                         options={"spkac": spkac,
                                                                  "template": template_name})
             certificate = crypto.dump_certificate(crypto.FILETYPE_PEM,
                                                   x509object)
         elif generate:
-            # Create the certificate on behalf of another user.
-            # Now we need to create the key pair,
-            # the request
-            # and the certificate
-            # We need the user for whom the certificate should be created
+            """
+            Create the certificate on behalf of another user. Now we need to create 
+            * the key pair,
+            * the request
+            * and the certificate
+            We need the user for whom the certificate should be created
+            """
             user = get_user_from_param(param, optionalOrRequired=required)
-
             keysize = getParam(param, "keysize", optional, 2048)
             key = crypto.PKey()
             key.generate_key(crypto.TYPE_RSA, keysize)
             req = crypto.X509Req()
             req.get_subject().CN = user.login
-            # Add email to subject
-            if user.info.get("email"):
-                req.get_subject().emailAddress = user.info.get("email")
-            # TODO: How can we define, which parameters should be added to CSR?
-            # req.get_subject().organizationalUnitName = user.realm
-            # TODO: Add Country, Organization, Email
+            # Add components to subject
+            if subject_components:
+                if "email" in subject_components and user.info.get("email"):
+                    req.get_subject().emailAddress = user.info.get("email")
+                if "realm" in subject_components:
+                    req.get_subject().organizationalUnitName = user.realm
+            # TODO: Add Country, Organization
             """
             req.get_subject().countryName = 'xxx'
             req.get_subject().stateOrProvinceName = 'xxx'


Test patch:
diff --git a/tests/test_api_lib_policy.py b/tests/test_api_lib_policy.py
--- a/tests/test_api_lib_policy.py
+++ b/tests/test_api_lib_policy.py
@@ -49,7 +49,8 @@
                                            webauthntoken_enroll, webauthntoken_request,
                                            webauthntoken_allowed, check_application_tokentype,
                                            required_piv_attestation, check_custom_user_attributes,
-                                           hide_tokeninfo, init_ca_template, init_ca_connector)
+                                           hide_tokeninfo, init_ca_template, init_ca_connector,
+                                           init_subject_components)
 from privacyidea.lib.realm import set_realm as create_realm
 from privacyidea.lib.realm import delete_realm
 from privacyidea.api.lib.postpolicy import (check_serial, check_tokentype,
@@ -3258,9 +3259,11 @@ def test_50_enroll_ca_connector(self):
         req.all_data = {"user": "cornelius", "realm": "home", "type": "certificate", "genkey": 1}
         init_ca_connector(req)
         init_ca_template(req)
+        init_subject_components(req)
         # Check, if that there is no parameter set
         self.assertNotIn("ca", req.all_data)
         self.assertNotIn("template", req.all_data)
+        self.assertNotIn("subject_components", req.all_data)
 
         # now create a policy for the CA connector and the template
         set_policy(name="ca",
@@ -3269,28 +3272,44 @@ def test_50_enroll_ca_connector(self):
                      CERTIFICATE_ACTION.CA_CONNECTOR, "caconnector",
                      CERTIFICATE_ACTION.CERTIFICATE_TEMPLATE, "catemplate"
                    ))
+        set_policy(name="sub1",
+                   scope=SCOPE.ENROLL,
+                   action="{0!s}=email".format(
+                       CERTIFICATE_ACTION.CERTIFICATE_REQUEST_SUBJECT_COMPONENT))
+        set_policy(name="sub2",
+                   scope=SCOPE.ENROLL,
+                   action="{0!s}=email realm".format(
+                       CERTIFICATE_ACTION.CERTIFICATE_REQUEST_SUBJECT_COMPONENT))
         # request, that matches the policy
         req.all_data = {"user": "cornelius", "realm": "home", "type": "certificate", "genkey": 1}
         # check that the parameters were added
         init_ca_connector(req)
         init_ca_template(req)
+        init_subject_components(req)
         # Check, if that there is no parameter set
         self.assertIn("ca", req.all_data)
         self.assertIn("template", req.all_data)
+        self.assertIn("subject_components", req.all_data)
         self.assertEqual("caconnector", req.all_data.get("ca"))
         self.assertEqual("catemplate", req.all_data.get("template"))
+        self.assertIn("email", req.all_data.get("subject_components"))
+        self.assertIn("realm", req.all_data.get("subject_components"))
 
         # request, that matches the policy
         req.all_data = {"user": "cornelius", "realm": "home", "type": "hotp", "genkey": 1}
         # check that it only works for certificate tokens
         init_ca_connector(req)
         init_ca_template(req)
+        init_subject_components(req)
         # Check, if that there is no parameter set
         self.assertNotIn("ca", req.all_data)
         self.assertNotIn("template", req.all_data)
+        self.assertNotIn("subject_components", req.all_data)
 
         # delete policy
         delete_policy("ca")
+        delete_policy("sub1")
+        delete_policy("sub2")
 
 
 class PostPolicyDecoratorTestCase(MyApiTestCase):
diff --git a/tests/test_lib_tokens_certificate.py b/tests/test_lib_tokens_certificate.py
--- a/tests/test_lib_tokens_certificate.py
+++ b/tests/test_lib_tokens_certificate.py
@@ -455,7 +455,6 @@ def test_02b_success_request_with_attestation(self):
                          "<X509Name object '/CN=cn=cornelius'>")
         remove_token(self.serial2)
 
-
     def test_03_class_methods(self):
         db_token = Token.query.filter(Token.serial == self.serial1).first()
         token = CertificateTokenClass(db_token)
@@ -485,11 +484,10 @@ def test_04_create_token_on_server(self):
 
         # missing user
         self.assertRaises(ParameterError,
-                          token.update, {"ca": "localCA","genkey": 1})
+                          token.update, {"ca": "localCA", "genkey": 1})
 
         token.update({"ca": "localCA", "genkey": 1,
                       "user": "cornelius"})
-
         self.assertEqual(token.token.serial, self.serial3)
         self.assertEqual(token.token.tokentype, "certificate")
         self.assertEqual(token.type, "certificate")
@@ -501,10 +499,9 @@ def test_04_create_token_on_server(self):
         self.assertEqual("{0!r}".format(x509obj.get_issuer()),
                          "<X509Name object '/C=DE/ST=Hessen"
                          "/O=privacyidea/CN=CA001'>")
-        # Currently it is work in progress if we add the OU or not. So we check for both.
-        self.assertIn("{0!r}".format(x509obj.get_subject()),
-                      ["<X509Name object '/OU=realm1/CN=cornelius/emailAddress=user@localhost.localdomain'>",
-                       "<X509Name object '/CN=cornelius/emailAddress=user@localhost.localdomain'>"])
+        # No Email Address in the subject!
+        subject = x509obj.get_subject()
+        self.assertEqual("{0!s}".format(subject), "<X509Name object '/CN=cornelius'>")
 
         # Test, if the certificate is also completely stored in the tokeninfo
         # and if we can retrieve it from the tokeninfo
@@ -514,9 +511,7 @@ def test_04_create_token_on_server(self):
         self.assertEqual("{0!r}".format(x509obj.get_issuer()),
                          "<X509Name object '/C=DE/ST=Hessen"
                          "/O=privacyidea/CN=CA001'>")
-        self.assertIn("{0!r}".format(x509obj.get_subject()),
-                      ["<X509Name object '/OU=realm1/CN=cornelius/emailAddress=user@localhost.localdomain'>",
-                       "<X509Name object '/CN=cornelius/emailAddress=user@localhost.localdomain'>"])
+        self.assertEqual("{0!r}".format(x509obj.get_subject()), "<X509Name object '/CN=cornelius'>")
 
         privatekey = token.get_tokeninfo("privatekey")
         self.assertTrue(privatekey.startswith("-----BEGIN PRIVATE KEY-----"))
@@ -527,6 +522,21 @@ def test_04_create_token_on_server(self):
         # revoke the token
         r = token.revoke()
         self.assertEqual(r, int_to_hex(x509obj.get_serial_number()))
+        remove_token(self.serial3)
+
+        # Now create a new token again and also check for subjects with email and realm
+        db_token = Token(self.serial3, tokentype="certificate")
+        db_token.save()
+        token = CertificateTokenClass(db_token)
+        token.update({"ca": "localCA", "genkey": 1,
+                      "subject_components": ["email", "realm"],
+                      "user": "cornelius"})
+        detail = token.get_init_detail()
+        certificate = detail.get("certificate")
+        x509obj = crypto.load_certificate(crypto.FILETYPE_PEM, certificate)
+        self.assertEqual("{0!r}".format(x509obj.get_subject()),
+                         "<X509Name object '/OU=realm1/CN=cornelius/emailAddress=user@localhost.localdomain'>")
+        remove_token(self.serial3)
 
     def test_05_get_default_settings(self):
         params = {}