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

HttpClient: Adding SSL Certification Verification #3176

Closed
wants to merge 17 commits into from

Conversation

salqadri
Copy link

@salqadri salqadri commented Apr 28, 2017

This PR adds a new API called "setRootCA" in which the user can pass in the root CA with which to validate an SSL connection.

WiFiClientSecure enables users to add multiple certificates for validation. But the underlying axTLS library actually only cares for the deepest certificate for verification. As such, I decided to have a much simpler API on HttpClient of setting a single certificate to prevent users from getting confused on how many certs they need to add. I call it Root to indicate that this must be the deepest certificate.

Unfortunately, the existing implementation of HttpClient was not conducive to what I wanted to do. I wanted the following API experience:
http.begin("https://www.google.com", 443); http.setRootCA(Equifax_cer, Equifax_cer_len); int result = http.GET();
But the problem was that both the connect call and the actual http request were both done in the path for http.GET. axTLS Certificate validation requires a TCP connection to first exist, and THEN you can load a certificate (I spent several hours figuring this and other issues like the fact that you HAVE to initialize sntp before validation can happen out by digging deep into the axTLS codebase).

So given this issue, I tried to solve this problem without causing API changes. The result was that I moved the connect phase into the begin command, and added a new state variable called _verified to determine if verification has already been done to avoid doing it again on subsequent connections.

I've also put in an example ino test file that anyone should be able to run to see how this works. One key thing to note is that you HAVE to initialize sntp before attempting validation because the certificate is validated with respect to time.

@codecov-io
Copy link

codecov-io commented Apr 29, 2017

Codecov Report

❗ No coverage uploaded for pull request base (master@eb891cd). Click here to learn what that means.
The diff coverage is n/a.

Impacted file tree graph

@@           Coverage Diff            @@
##             master   #3176   +/-   ##
========================================
  Coverage          ?   27.6%           
========================================
  Files             ?      20           
  Lines             ?    3655           
  Branches          ?     678           
========================================
  Hits              ?    1009           
  Misses            ?    2468           
  Partials          ?     178

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update eb891cd...6c2a8a3. Read the comment docs.

Copy link
Member

@igrr igrr left a comment

Choose a reason for hiding this comment

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

Overall this is a valuable feature, however please revert the old behaviour of begin methods.

clear();

// Remove protocol if any
Copy link
Member

Choose a reason for hiding this comment

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

This method takes host as an argument, i.e. just the DNS name or IP address. It should not expect to see a protocol prefix.

Copy link
Author

Choose a reason for hiding this comment

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

Makes the function more resilient for me. It is also more consistent with the other overload, making it more easy to swap out the API a developer may want to use.

{
bool result = false;
if(_tcp && (_protocol == "https" || _port == 443)) {
auto client = static_cast<WiFiClientSecure*>(_tcp.get());
Copy link
Member

Choose a reason for hiding this comment

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

If you follow the existing TransportTraits pattern, you can create a new TransportTraits class which performs root certificate verification instead of fingerprint verification. In setRootCA method, you can set _transportTraits to an instance of, say, new TLSRootCertTraits(cert, size), which will then perform verification using the existing interface.

Copy link
Author

Choose a reason for hiding this comment

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

The approach you suggest would be more expensive as it requires storing the cert, which can be quite large.

}
_tcp = _transportTraits->create();

if(!_tcp->connect(_host.c_str(), _port)) {
Copy link
Member

Choose a reason for hiding this comment

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

This is a fairly big behavioural change, and it's not hard to come up with a bunch of cases when it will break existing sketches. If you need to defer setting the CA until the time connection is established, save the CA information in the appropriate Traits structure (probably a new one, dedicated to verification using a root CA cert). Then use this information when the verification has to take place. If HTTPClient needs to interact with Traits class at some other point in the process, add a new interface method and implement it as needed. It doesn't look like it is worth changing the behaviour of user-facing class, when the problem can be solved inside the implementation.

Copy link
Author

Choose a reason for hiding this comment

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

No saving the CA is a bad idea as it would consume a lot of RAM. I've been trying to explicitly avoid storing the CA.

Copy link
Member

Choose a reason for hiding this comment

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

OK, that is a fair observation.
Note that if the certificate is loaded from file, we need to have a copy of it in RAM at some point anyway to pass this copy into axTLS. Same for PROGMEM. So in these two cases, there is no optimization possible (which wouldn't involve changing axTLS to be capable of reading from streams).
In the case when the certificate is already in RAM (such as in your example) we can store the pointer to the certificate and pass it on to axTLS without making a copy. This however is likely the least useful option, because certificate places into .rodata will take up precious RAM during the entire application life. On the other hand, the certificate in FS or PROGMEM will be in RAM only when it is actually needed there — during the TLS connection phase.

Copy link
Author

@salqadri salqadri May 30, 2017

Choose a reason for hiding this comment

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

Agreed. What I'm trying to do is make it a temporary cost; you only need it in RAM during the verification phase, and then you can unload it from RAM. Something like this:
SPIFFS.begin(); File ca = SPIFFS.open(F("/Equifax.cer"), "r"); if (ca) { if (!http->setRootCA(ca, ca.size())) { Serial.println(F("Root CA failed to load")); } ca.close(); } SPIFFS.end();

* Copyright (c) 2017 Syed Salman Qadri. All rights reserved.
* This file is part of the ESP8266HTTPClient for Arduino.
*
* This library is free software; you can redistribute it and/or
Copy link
Member

Choose a reason for hiding this comment

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

examples are normally licensed as public domain or CC0


// This is a binary dump of the root CA.
// You can also choose to load the cert via a file stream.
// Here are the steps I did to dump the CA on my Mac:
Copy link
Member

Choose a reason for hiding this comment

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

Suggest writing this in a slightly more impersonal way :)

Copy link
Member

Choose a reason for hiding this comment

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

Also, what steps would users on other platforms take? Windows is probably the biggest question. Instructions for Linux will likely be the same, except for Mac-specific names ("Keychain Access", "Terminal").

Copy link

@Defozo Defozo May 8, 2017

Choose a reason for hiding this comment

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

The exact same commands works on Windows too.

Copy link
Author

Choose a reason for hiding this comment

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

Good to know. I can update that it works on Mac and Windows.

// 2) Open Keychain Access. In the Search bar type "Equifax" and press Enter.
// 3) Right-click the cert and click Export and save it as a .cer.
// 4) In the terminal, type xxd -i <path_to_cer_file>
unsigned char Equifax_cer[] = {
Copy link
Member

Choose a reason for hiding this comment

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

Can this be put in PROGMEM? We also need to support setRootCA_P in this case.

Copy link
Author

Choose a reason for hiding this comment

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

Great idea. Maybe we can do that in a subsequent PR.

0x70, 0xf1, 0x94, 0x21, 0x65, 0x44, 0x5f, 0xfa, 0xfc, 0xef, 0x29, 0x68,
0xa9, 0xa2, 0x87, 0x79, 0xef, 0x79, 0xef, 0x4f, 0xac, 0x07, 0x77, 0x38
};
unsigned int Equifax_cer_len = 804;
Copy link
Member

Choose a reason for hiding this comment

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

suggest sizeof(Equifax_cer) / sizeof(Equifax_cer[0]) here.

Copy link
Author

Choose a reason for hiding this comment

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

These two lines are generated by the previous command, so I'm trying to show that you can just copy-paste verbatim.

@salqadri
Copy link
Author

@igrr I hadn't noticed that you guys provided feedback on my PR until now :). Thanks! I'll happily make the various fixes and redo it, but only if we don't make it so that we have to store the cert in the traits. That approach consumes too much memory, and I'd rather give the developer more control on how they wish the cert to be stored. Whether the developer is storing it in a file system, or in memory, either way the traits approach leads to duplication which causes crashes.

@salqadri
Copy link
Author

salqadri commented Jun 10, 2017

@igrr My branch seems to be broken after I merged from master. SSL cert verification is no longer working for me. Sometime after c5c138e and between 8166263 something you did has caused my fork to now fail. It seems that there were a lot of changes to various SSL aspects, but I'm having a hard time understanding what you were trying to do. Could you possibly explain the changes to things like WiFiClientSecure etc. so that I can figure out what broken?

EDIT: Ok I isolated the issue to this commit: 1d41859

EDIT: I was able to reproduce the issue. It seems to happen for a particular cert. I created an issue with reproduction steps using your own sample here: #3340

Please fix this asap, since the bug is in axTLS and it is too complex for me to fix myself.

@salqadri
Copy link
Author

I have now fixed this branch by getting everything from master except that axtls library update. So I'm using the version of axtls from prior to the May 9 2017 update. Would love it if @igrr you can fix axtls so that I don't have to do this workaround.

… which broke the ability to do SSL validation with Google services"

This reverts commit 92219f6.
@Defozo
Copy link

Defozo commented Aug 8, 2017

When it'll be merged?

… 9 2017, which broke the ability to do SSL validation with Google services""

This reverts commit bb7ba50.
@cimba007
Copy link

Any update so far?

@cimba007
Copy link

@salqadri: Any chance to fix this?

@earlephilhower
Copy link
Collaborator

I believe this PR is superseded by #4980 which lets you stick in a custom configured WiFiClient* to HTTPClient.

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.

6 participants