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

Implement discussed changes in #735. #737

Open
wants to merge 1 commit into
base: v3.3.0.4
Choose a base branch
from

Conversation

certxlm
Copy link

@certxlm certxlm commented Oct 3, 2019

Added 3 new Client context configuration:

  • transport_ca_certificate: a list of PEM certificates to valide the
    frontend certificate (transport)
  • transport_private_key: a private key for client cert authentication
  • transport_public_certificate: the certificate for client cert
    authentication

Added 3 new Client context configuration:
- transport_ca_certificate: a list of PEM certificates to valide the
frontend certificate (transport)
- transport_private_key: a private key for client cert authentication
- transport_public_certificate: the certificate for client cert
authentication
@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here with @googlebot I signed it! and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

ℹ️ Googlers: Go here for more info.

@certxlm
Copy link
Author

certxlm commented Oct 3, 2019

@googlebot I signed it!

@googlebot
Copy link

All (the pull request submitter and all commit authors) CLAs are signed, but one or more commits were authored or co-authored by someone other than the pull request submitter.

We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that by leaving a comment that contains only @googlebot I consent. in this pull request.

Note to project maintainer: There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent. In those cases, you can manually confirm consent of the commit author(s), and set the cla label to yes (if enabled on your project).

ℹ️ Googlers: Go here for more info.

@mathieubaeumler
Copy link

@googlebot I consent.

@googlebot
Copy link

CLAs look good, thanks!

ℹ️ Googlers: Go here for more info.

Copy link
Collaborator

@mbushkov mbushkov left a comment

Choose a reason for hiding this comment

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

Thank you for this PR. The code generally looks good, but I think we can make it a bit simpler and cleaner. Please see my comments below.

Also, an important issue: this functionality has to be tested in order to be submitted. We have an integration test for AdminUI that instantiates an HTTPS server (

class ApiSslServerTestBase(test_lib.GRRBaseTest, acl_test_lib.AclTestMixin):
). I wonder if we can use a similar approach to test comms changes introduced here?

At the very least, we have to augment

class HTTPManagerTest(test_lib.GRRBaseTest):
to make sure expected parameters are passed to "requests" library when OpenURL is called with newly introduced arguments

Comment on lines +197 to +199
transport_cert=None,
transport_key=None,
transport_cacert=None,
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's a bunch of places where you have to pass transport_cert, transport_key and transport_cacert. Would it make sense to group them into a class? You can use namedtuple for that (like it's done here:

ParsedRelease = collections.namedtuple('ParsedRelease', 'release, major, minor')
)

Maybe makes sense to name the data structure TransportSecurity?

Choose a reason for hiding this comment

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

indeed, already working on that point, should be able to provide it soon

Comment on lines +215 to +217
transport_cert: The certificate to encrypt communications at the transport level
transport_key: The private key to encrypt communications at the transport level
transport_cacert: The path to the trusted ca certificates files, default to certifi.where()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please see my comment above. I think it'd make sense to group these 3 arguments into a single one.

Comment on lines +236 to +238
transport_cert=transport_cert,
transport_key=transport_key,
transport_cacert=transport_cacert,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please see the comment above.

Comment on lines +267 to +269
transport_cert=None,
transport_key=None,
transport_cacert=None,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please see the comment above.

Comment on lines +1535 to +1546
def DEFINE_semantic_value_list(self, semantic_type, name, default=None, help=""):
if issubclass(semantic_type, rdf_structs.RDFStruct):
raise ValueError("DEFINE_semantic_value_list should be used for types "
"based on RDFValues.")
self.AddOption(
type_info.RDFValueList(
rdfclass=semantic_type,
name=name,
default=default,
validator=type_info.RDFValueType(rdfclass=semantic_type),
description=help))

Copy link
Collaborator

Choose a reason for hiding this comment

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

IIUC, there's a canonical text representation for certs chains as text. So instead of specifying a list of certificates in GRR configuration, you can likely put the whole chain in a canonical format as a string. This way you won't have to introduce the notion of semantic_value_list. Please see my comment below regarding RDFX509CertChain.

Choose a reason for hiding this comment

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

That, on the other hand, I'm really not sure how to tackle.
The only way I know of to represent a cert chain is to actually simply concatenate all the involved certs (think /etc/ssl/certs/ca-certificates.crt).

Which basically makes it a list of x509 certificates, hence that semantic_value_list (which, admittedly, is an horrendous hack which I'm not proud of).

So, could you provide me with some pointers on what exactly is this canonical text representation you're mentioning? Then I'll be glad to implement the requested change and get rid of the list :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry for a delay with the reply. By canonical text representation I indeed mean a simple concatenation of all the certs.

In your current PR you expect a list of certificates in the configuration and then you concatenate them and pass to the communication layer. But GRR doesn't need to access each individual certificate, it will concatenate them anyway and delegate everything to the "requests" library. With a type like a suggested RDFX509CertChain, you can provide the certificates chain as a concatenated string right in the config and then simply store it in a file and and pass further.

To be fair, I'd also see no problem with simply using a string type and DEFINE_string for Client.transport_ca_certificate. You can then simply read the chain from the config option, put it into a temp file and pass it on.

What do you think?

Comment on lines +999 to +1015
self.transport_cacerts = None
try:
if transport_cacert is None:
logging.debug("loading Client.transport_ca_certificate from configuration file ...")
transport_cacert = config.CONFIG.Get("Client.transport_ca_certificate", default=None)
if transport_cacert is not None:
pem_path = os.path.join(config.CONFIG["Client.install_path"], "certs", "cacert.pem")
logging.debug("dumping Client.transport_ca_certificate to %s ..." % pem_path)
with open(pem_path, "wb") as fd:
for cert in transport_cacert:
fd.write(cert.AsPEM().encode("utf-8"))
fd.write(os.linesep)
self.transport_cacert = pem_path
else:
logging.debug("Client.transport_ca_certificate is not set, defaulting to bundled CA")
except Exception as e:
logging.error("unable to save transport cacert: %s", e)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This code and the code below feels repeatable. Can we refactor it into a function?

Comment on lines +1007 to +1010
with open(pem_path, "wb") as fd:
for cert in transport_cacert:
fd.write(cert.AsPEM().encode("utf-8"))
fd.write(os.linesep)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Which permissions is the file going to be created with?
I think it's cleaner to use tempfile.NamedTemporaryFile. This way you also won't have to care about the "certs" subfolder in Client.install_path existing or not and there will be no need to modify the client builder code.

Choose a reason for hiding this comment

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

That is a nice improvement also, and with delete=true, might even ensure the files are cleaned when grr is stopped. I'll work on that too.

Comment on lines +1294 to +1302
transport_cert=self.transport_cert
transport_key=self.transport_key
transport_cacert=self.transport_cacert
response = self.http_manager.OpenServerEndpoint(
"server.pem", verify_cb=self.VerifyServerPEM)
"server.pem",
verify_cb=self.VerifyServerPEM,
transport_cert=transport_cert,
transport_cacert=transport_cacert,
transport_key=transport_key)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why can't we pass self.transport_cert, self.transport_key, self.transport_cacert directly? Why do we need to assign them to variables first?

Comment on lines +1151 to +1159
transport_cert=self.transport_cert
transport_key=self.transport_key
transport_cacert=self.transport_cacert
# Verify the response is as it should be from the control endpoint.
response = self.http_manager.OpenServerEndpoint(
path="control?api=%s" % config.CONFIG["Network.api"],
transport_cert=transport_cert,
transport_key=transport_key,
transport_cacert=transport_cacert,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why can't we pass self.transport_cert, self.transport_key, self.transport_cacert directly? Why do we need to assign them to variables first?

Comment on lines +373 to +387
class RDFValueList(List):
"""A List of RDFValue."""
class _validator:
def __init__(self, validator=None):
self.validator = validator
def Validate(self, val):
try:
return self.validator.Validate(val.encode('utf-8'))
except Exception as e:
raise TypeValueError("Invalid data: %s" % e)
def __init__(self, rdfclass=None, validator=None, **kwargs):
super(RDFValueList, self).__init__(**kwargs)
self.validator = self._validator(validator)
self._type = self.rdfclass = rdfclass

Copy link
Collaborator

Choose a reason for hiding this comment

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

This will make an already complicate GRR type info system even more complicated :)
What I'd suggest instead is to implement an RDFX509CertChain class that would know how to parse from/serialize into a canonical representation of a cert chain. The call can be done similarly to the RDFX509Cert (

class RDFX509Cert(rdfvalue.RDFPrimitive):
). But instead of having cert-specific methods, it will have a "certs" property that will return a list of certs in the chain (each having RDFX509Cert type).
Also, this class would have to be tested to ensure parsing works as expected.

@mathieubaeumler
Copy link

One last note, I'll try to find some time to implement the tests in comms_test.py and if possible ssl_test.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants