-
Notifications
You must be signed in to change notification settings - Fork 760
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
base: v3.3.0.4
Are you sure you want to change the base?
Conversation
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
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 What to do if you already signed the CLAIndividual signers
Corporate signers
ℹ️ Googlers: Go here for more info. |
@googlebot I signed it! |
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 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 ℹ️ Googlers: Go here for more info. |
@googlebot I consent. |
CLAs look good, thanks! ℹ️ Googlers: Go here for more info. |
There was a problem hiding this 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): |
At the very least, we have to augment
class HTTPManagerTest(test_lib.GRRBaseTest): |
transport_cert=None, | ||
transport_key=None, | ||
transport_cacert=None, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
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() |
There was a problem hiding this comment.
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.
transport_cert=transport_cert, | ||
transport_key=transport_key, | ||
transport_cacert=transport_cacert, |
There was a problem hiding this comment.
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.
transport_cert=None, | ||
transport_key=None, | ||
transport_cacert=None, |
There was a problem hiding this comment.
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.
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)) | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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?
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) |
There was a problem hiding this comment.
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?
with open(pem_path, "wb") as fd: | ||
for cert in transport_cacert: | ||
fd.write(cert.AsPEM().encode("utf-8")) | ||
fd.write(os.linesep) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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) |
There was a problem hiding this comment.
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?
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, |
There was a problem hiding this comment.
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?
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 | ||
|
There was a problem hiding this comment.
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): |
Also, this class would have to be tested to ensure parsing works as expected.
One last note, I'll try to find some time to implement the tests in comms_test.py and if possible ssl_test.py |
Added 3 new Client context configuration:
frontend certificate (transport)
authentication