-
Notifications
You must be signed in to change notification settings - Fork 13.3k
HttpClient: Adding SSL Certification Verification #3176
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
Conversation
Codecov Report
@@ 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.
|
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.
Overall this is a valuable feature, however please revert the old behaviour of begin
methods.
clear(); | ||
|
||
// Remove protocol if any |
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 method takes host
as an argument, i.e. just the DNS name or IP address. It should not expect to see a protocol prefix.
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.
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()); |
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.
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.
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.
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)) { |
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 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.
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.
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.
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.
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.
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.
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 |
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.
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: |
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.
Suggest writing this in a slightly more impersonal way :)
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.
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").
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.
The exact same commands works on Windows too.
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.
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[] = { |
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.
Can this be put in PROGMEM? We also need to support setRootCA_P in this case.
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.
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; |
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.
suggest sizeof(Equifax_cer) / sizeof(Equifax_cer[0])
here.
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.
These two lines are generated by the previous command, so I'm trying to show that you can just copy-paste verbatim.
@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. |
@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. |
…roke the ability to do SSL validation with Google services
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.
When it'll be merged? |
… 9 2017, which broke the ability to do SSL validation with Google services"" This reverts commit bb7ba50.
Any update so far? |
@salqadri: Any chance to fix this? |
I believe this PR is superseded by #4980 which lets you stick in a custom configured |
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.