Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client-side CSR autogeneration #10

Closed
wants to merge 21 commits into from

Conversation

LiptonB
Copy link
Contributor

@LiptonB LiptonB commented Aug 22, 2016

Adds a library that builds scripts that builds CSRs. Adds a CLI command, 'cert-get-requestdata', that uses this library and builds the script for a given principal. Adds rules for the caIPAserviceCert profile, as well as a new userCert profile, stored in json files in /usr/share/ipa/csr. The rule provider is a separate class so that it can be replaced easily if we ever want to move rules to the server side.

https://fedorahosted.org/freeipa/ticket/4899

@@ -503,6 +503,7 @@ Requires: python-custodia
Requires: python-dns >= 1.11.1
Requires: python-netifaces >= 0.10.4
Requires: pyusb
Requires: python-jinja2
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about BuildRequires? pylint would be failing on the system where python-jinja2 is not installed.

Copy link
Contributor Author

@LiptonB LiptonB Aug 26, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had that before, but removed it when I couldn't get pylint to actually complain about the missing import. But maybe that was just because "import-error" is disabled (twice?) in pylintrc. Anyway, added as a fixup commit.

Edit: force pushed to fix conflicts with master

@LiptonB
Copy link
Contributor Author

LiptonB commented Aug 31, 2016

As discussed elsewhere, this script generation is a fairly low-level operation; you have to specify the helper and know how to run the script. Most users will probably want a command that just takes in a private key location and a profile and requests the cert for them. In case you'd like to look at it now, I have an implementation of that in a separate branch, which I'll create a pull request for after this one is complete: master...LiptonB:local-cert-build

@LiptonB
Copy link
Contributor Author

LiptonB commented Sep 6, 2016

I've added a commit (Use data_sources option to define which fields are rendered) that simplifies the way we avoid rendering rules whose source data are missing, as discussed here: https://www.redhat.com/archives/freeipa-devel/2016-September/msg00051.html. I prefer this approach to the macros in the original implementation, but I'm leaving it as a separate commit in case you would like to compare them.

@LiptonB
Copy link
Contributor Author

LiptonB commented Sep 14, 2016

Some tests for the CSR generation functionality have been added to the pull request.

)
)

def forward(self, *args, **options):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be execute, the command is not forwarded to the server.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used forward because the default run method only calls execute if you're running in the server context. Overriding forward is what other client-only commands like vault-add do. Should I override run instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to mention that you should also inherit from Local. (Sorry.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I had tried that before but abandoned it because the rpc client wasn't connected automatically with Local. But now I see that vault-add connects it explicitly, so I've updated my code to do the same.

stored mapping rules.
""")

CSR_DATA_DIR = '/usr/share/ipa/csr'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File paths should be defined in ipaplatform.paths.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


from ipalib import errors
from ipalib.text import _
from ipapython.ipa_log_manager import root_logger
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not log into the root logger, use a module-specific logger:

from ipapython.ipa_log_manager import log_mgr

logger = log_mgr.get_logger(__name__)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I thought this might not be right but I didn't know what the right way was.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, this is not used often in our code, but we try to follow common best practices in new code, and the best practice for logging seems to be to use module-specific loggers.

import collections
import jinja2
import jinja2.ext
import jinja2.sandbox
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order of imports should be:

  1. standard
  2. 3rd party
  3. ipa

Copy link
Contributor Author

@LiptonB LiptonB Sep 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. It seems to be a convention that six is the last import so I've left it that way. Is that right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, six should be imported together with other 3rd party imports.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, moved six to the proper place.


try:
rule = [r for r in ruleset['rules']
if r['helper'] == helper][0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The [0] does not look right. Either use the complete list, or fail if there is more than one item.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should only be one, so I'll raise an error otherwise.

except IOError:
raise errors.CertificateMappingError(
reason=_('Ruleset %(ruleset)s is configured in profile but'
' does not exist.') % {'ruleset': rule_name})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks rather like errors.NotFound.

Also, this method has no knowledge of profiles, so how do you know the ruleset is configured in profile?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points.

raise errors.CertificateMappingError(
reason=_('No transformation in "%(ruleset)s" rule supports'
' helper "%(helper)s"') %
{'ruleset': rule_name, 'helper': helper})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks rather like errors.EmptyResult.

except IOError:
raise errors.CertificateMappingError(
reason=_('No certificate mappings are defined for profile'
' %(profile_id)s') % {'profile_id': profile_id})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also looks rather like errors.NotFound.

@@ -1697,6 +1697,15 @@ class CertificateFormatError(CertificateError):
format = _('Certificate format error: %(error)s')


class CertificateMappingError(CertificateError):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we do without adding yet another god exception? See also my other exception related comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, the meaning of this exception drifted a lot. After making the changes you suggest above, the remaining cases are:

  1. A mapping rule contains more than one stanza that matches the given helper. This is very close to SingleMatchExpected, but the format string for that exception doesn't really make sense for this case.
  2. The jinja2 render failed (probably means the template was written incorrectly). I can't find any errors that seem appropriate for this at all.
  3. Nothing was generated for a rule that's marked as "required". RequirementError looks almost right, but the error message ("ERROR: 'syntaxSubject' is required") could be confusing. Admittedly, the real problem with this error message isn't just that it's generic but that it doesn't tell you what the actual missing data are, and that's trickier to solve.

Do you have any advice for what to do about these remaining cases?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. You can subclass SingleMatchExpected with your own format string.
  2. I would use a new ExecutionError subclass for this.
  3. RequirementError is not a good choice, as it is a validation error, but what you are describing is an execution error.

Copy link
Contributor Author

@LiptonB LiptonB Oct 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of this version (0e999cf)? I subclassed SingleMatchExpected for the case of multiple matches for the helper, and merged the other two into CSRTemplateError with different descriptions.

@HonzaCholasta
Copy link
Contributor

In addition to my inline comments above:

  1. "Certificate mapping" does not really evoke "certificate request templating" to me, and is also used in the context of mapping identities to certificates. Could we use a more suitable name to avoid confusion?
  2. The ipalib.certmapping module is used only in ipaclient, so that's where it should be located. It can be moved to ipalib later if necessary.
  3. I don't think IPAExtension deserves it's own module, at least not now.

Copy link
Contributor Author

@LiptonB LiptonB left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comments! I've fixed the simple ones and replied to the rest. Regarding your comments about file organization:

  1. I quite agree that certmapping isn't a good name for what this turned out to be. With the convention of naming modules after the objects they model, perhaps a good name would be certrequest or csr? The command could be renamed to something like certrequest-get-data (or certrequest-get-script).
  2. Just to confirm, you're suggesting just moving these classes to the ipaclient.plugins.<whatever we end up calling it> module?
  3. Seems reasonable, I've moved it into the ipalib module for now. It will go wherever the contents of that module end up.

Logistical stuff:

  • Now that this is under review I won't add any more content. Are you ok with the two commits about testing being part of this review or should I remove them?
  • If you run rebase --autosquash with the latest commit it doesn't actually apply cleanly, but I'm trying not to change history while it's being reviewed, so I'll do the rebase later on if that's ok?

)
)

def forward(self, *args, **options):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used forward because the default run method only calls execute if you're running in the server context. Overriding forward is what other client-only commands like vault-add do. Should I override run instead?

import collections
import jinja2
import jinja2.ext
import jinja2.sandbox
Copy link
Contributor Author

@LiptonB LiptonB Sep 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. It seems to be a convention that six is the last import so I've left it that way. Is that right?


from ipalib import errors
from ipalib.text import _
from ipapython.ipa_log_manager import root_logger
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I thought this might not be right but I didn't know what the right way was.

stored mapping rules.
""")

CSR_DATA_DIR = '/usr/share/ipa/csr'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

except IOError:
raise errors.CertificateMappingError(
reason=_('Ruleset %(ruleset)s is configured in profile but'
' does not exist.') % {'ruleset': rule_name})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points.


try:
rule = [r for r in ruleset['rules']
if r['helper'] == helper][0]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should only be one, so I'll raise an error otherwise.

@@ -1697,6 +1697,15 @@ class CertificateFormatError(CertificateError):
format = _('Certificate format error: %(error)s')


class CertificateMappingError(CertificateError):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, the meaning of this exception drifted a lot. After making the changes you suggest above, the remaining cases are:

  1. A mapping rule contains more than one stanza that matches the given helper. This is very close to SingleMatchExpected, but the format string for that exception doesn't really make sense for this case.
  2. The jinja2 render failed (probably means the template was written incorrectly). I can't find any errors that seem appropriate for this at all.
  3. Nothing was generated for a rule that's marked as "required". RequirementError looks almost right, but the error message ("ERROR: 'syntaxSubject' is required") could be confusing. Admittedly, the real problem with this error message isn't just that it's generic but that it doesn't tell you what the actual missing data are, and that's trickier to solve.

Do you have any advice for what to do about these remaining cases?

@HonzaCholasta
Copy link
Contributor

  1. I'm afraid certrequest (actually certreq) is already taken. What about csrgen?
  2. I would be perfectly happy with ipaclient.<whatever>.
  3. OK.

Logistical stuff:

  • I'm fine with the testing commits being part of this review. I would also be fine with new content if it was added after this part of review is done.
  • I'm fine with later rebase.

@LiptonB
Copy link
Contributor Author

LiptonB commented Sep 19, 2016

csrgen sounds good to me. The new modules have now been moved to ipaclient.plugins.csrgen, ipaclient.csrgen, and ipatests.test_ipaclient.test_csrgen.

FYI: I force pushed a few minutes after adding these commits to fix a pep8 error.

@LiptonB
Copy link
Contributor Author

LiptonB commented Oct 3, 2016

@jcholast, when you get a chance, could you take another look at this and let me know what else is needed?

%dir %{_usr}/share/ipa/csr/profiles
%{_usr}/share/ipa/csr/profiles/*.json
%dir %{_usr}/share/ipa/csr/rules
%{_usr}/share/ipa/csr/rules/*.json
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we call the feature csrgen, I guess we should put the files into /usr/share/ipa/csrgen rather than /usr/share/ipa/csr.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, true. Done.

@HonzaCholasta
Copy link
Contributor

@LiptonB, I have added some inline comments.

Also, have you seen my latest reply in the design thread?

@LiptonB
Copy link
Contributor Author

LiptonB commented Oct 10, 2016

Thanks, I've updated the code based on your comments (force pushed to fix conflicts with master). And thanks for pointing out that email! I don't know how I missed it, but I will respond shortly.

frasertweedale added a commit to frasertweedale/freeipa that referenced this pull request Oct 14, 2016
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper,
we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to
parse the friendlyName out of CSRs generated by certmonger (it
contains the NSSDB nickname of the cert).

Use other information from the renewal helper process environment to
determine the nickname and remove our PKCS freeipa#10 pyasn1 spec.

Part of: https://fedorahosted.org/freeipa/ticket/6398
frasertweedale added a commit to frasertweedale/freeipa that referenced this pull request Oct 14, 2016
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper,
we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to
parse the friendlyName out of CSRs generated by certmonger (it
contains the NSSDB nickname of the cert).

Use other information from the renewal helper process environment to
determine the nickname and remove our PKCS freeipa#10 pyasn1 spec.

Part of: https://fedorahosted.org/freeipa/ticket/6398
frasertweedale added a commit to frasertweedale/freeipa that referenced this pull request Oct 20, 2016
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper,
we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to
parse the friendlyName out of CSRs generated by certmonger (it
contains the NSSDB nickname of the cert).

Use other information from the renewal helper process environment to
determine the nickname and remove our PKCS freeipa#10 pyasn1 spec.

Part of: https://fedorahosted.org/freeipa/ticket/6398
@LiptonB
Copy link
Contributor Author

LiptonB commented Oct 20, 2016

Updated to fix conflicts with master again. I'm not sure what's up with Travis, it seems to be checking out PR #109 instead of this one for the pep8 check:

$ cd freeipa/freeipa
$ git fetch origin +refs/pull/109/merge:
remote: Counting objects: 78053, done.
remote: Compressing objects: 100% (14671/14671), done.
remote: Total 78053 (delta 64375), reused 76890 (delta 63219), pack-reused 0
Receiving objects: 100% (78053/78053), 28.18 MiB | 18.03 MiB/s, done.
Resolving deltas: 100% (64375/64375), completed with 841 local objects.
From https://github.com/freeipa/freeipa
 * branch            refs/pull/109/merge -> FETCH_HEAD
$ git checkout -qf FETCH_HEAD

frasertweedale added a commit to frasertweedale/freeipa that referenced this pull request Oct 26, 2016
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper,
we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to
parse the friendlyName out of CSRs generated by certmonger (it
contains the NSSDB nickname of the cert).

Use other information from the renewal helper process environment to
determine the nickname and remove our PKCS freeipa#10 pyasn1 spec.

Part of: https://fedorahosted.org/freeipa/ticket/6398
frasertweedale added a commit to frasertweedale/freeipa that referenced this pull request Nov 9, 2016
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper,
we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to
parse the friendlyName out of CSRs generated by certmonger (it
contains the NSSDB nickname of the cert).

Use other information from the renewal helper process environment to
determine the nickname and remove our PKCS freeipa#10 pyasn1 spec.

Part of: https://fedorahosted.org/freeipa/ticket/6398
frasertweedale added a commit to frasertweedale/freeipa that referenced this pull request Nov 9, 2016
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper,
we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to
parse the friendlyName out of CSRs generated by certmonger (it
contains the NSSDB nickname of the cert).

Use other information from the renewal helper process environment to
determine the nickname and remove our PKCS freeipa#10 pyasn1 spec.

Part of: https://fedorahosted.org/freeipa/ticket/6398
frasertweedale added a commit to frasertweedale/freeipa that referenced this pull request Nov 10, 2016
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper,
we currently use our hand-rolled PKCS freeipa#10 pyasn1 specification to
parse the friendlyName out of CSRs generated by certmonger (it
contains the NSSDB nickname of the cert).

Use other information from the renewal helper process environment to
determine the nickname and remove our PKCS freeipa#10 pyasn1 spec.

Part of: https://fedorahosted.org/freeipa/ticket/6398
ghost pushed a commit that referenced this pull request Nov 10, 2016
In the dogtag-ipa-ca-renew-agent-submit certmonger renewal helper,
we currently use our hand-rolled PKCS #10 pyasn1 specification to
parse the friendlyName out of CSRs generated by certmonger (it
contains the NSSDB nickname of the cert).

Use other information from the renewal helper process environment to
determine the nickname and remove our PKCS #10 pyasn1 spec.

Part of: https://fedorahosted.org/freeipa/ticket/6398

Reviewed-By: Jan Cholasta <jcholast@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Adds a library that uses jinja2 to format a script that, when run, will
build a CSR. Also adds a CLI command, 'cert-get-requestdata', that uses
this library and builds the script for a given principal. The rules are
read from json files in /usr/share/ipa/csr, but the rule provider is a
separate class so that it can be replaced easily.

https://fedorahosted.org/freeipa/ticket/4899
This removes the ipa.syntaxrule and ipa.datarule macros in favor of
simple 'if' statements based on the data referenced in the rules. The
'if' statement for a syntax rule is generated based on the data rules it
contains.

The Subject DN should not be generated unless all data rules are in
place, so the ability to override the logical operator that combines
data_sources (from 'or' to 'and') is added.
Some tests are failing and will be fixed in the next commit.
This patch also contains some code changes to make the code easier to
test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants