ssl: support wildcard matching for verify_subject_alt_name#1298
ssl: support wildcard matching for verify_subject_alt_name#1298crazytan wants to merge 6 commits intoenvoyproxy:masterfrom crazytan:master
Conversation
mattklein123
left a comment
There was a problem hiding this comment.
Beyond doc/format failures, small perf comments. @myidpt could you take a quick look?
source/common/ssl/context_impl.cc
Outdated
There was a problem hiding this comment.
perf nit: If you are going to call strlen() you might as well just walk both strings at the same time char by char. Also, probably don't need to do the direct compare first and then do this (could just do direct compare in same code path probably) but that's not as big of a deal to me.
There was a problem hiding this comment.
agree that the code is not very efficient. Will rewrite so that it only takes one walk through both strings.
source/common/ssl/context_impl.cc
Outdated
There was a problem hiding this comment.
I don't think you can always safely dereference *(p - 1). Please add test for this while fixing.
source/common/ssl/context_impl.cc
Outdated
There was a problem hiding this comment.
This whole function is low-level C instead of C++11.
Please consider using std::string::back() and std::string::compare(0, uriPattern.length() - 1, uri)
There was a problem hiding this comment.
Yes that would be much better. :)
There was a problem hiding this comment.
I'm confused but std::string::compare is what I was using. So should I use pointer walk through or string::compare?
There was a problem hiding this comment.
You should use std::string::back() - O(1), std::string::length() - O(1) and std::string::compare() - O(n).
Previously, you were using strlen() - O(n) and std::string::compare() - O(n), i.e. walking the same string twice, which resulted in suboptimal performance, although the difference is probably negligible.
There was a problem hiding this comment.
got it. Since my original strlen() was unnecessary, I'll change back to string::compare() to match c++11 style so that future change will be easier.
|
@crazytan looks like you fixed the bug. Anyway, this impl is OK with me, but also happy to have it redone the way @PiotrSikora mentioned. I will let the two of you sort it out. |
source/common/ssl/context_impl.cc
Outdated
| bool ContextImpl::uriMatch(const std::string& uriPattern, const char* uri) { | ||
| size_t pattern_len = uriPattern.length(); | ||
| if (pattern_len > 1 && uriPattern[pattern_len - 1] == '*' && uriPattern[pattern_len - 2] == '/') { | ||
| return uriPattern.compare(0, pattern_len - 1, uri, pattern_len - 1) == 0; |
There was a problem hiding this comment.
I think you will need to use strncmp() here since IIRC compare will go off end of string. (See ASAN failure).
There was a problem hiding this comment.
That's because the wrong method is used here. It should be:
uriPattern.compare(0, pattern_len - 1, uri)
for NULL-terminated string, instead of:
uriPattern.compare(0, pattern_len - 1, uri, pattern_len - 1)
which specifies length of the uri string.
There was a problem hiding this comment.
the problem here is, if uriPattern="foo.bar/*" and uri="foo.bar/baz", uriPattern.compare(0, pattern_len - 1, uri) would give negative result -3. And this cannot be distinguished from the case as uriPattern="foo.aar" and uri="foo.bar", for which compare gives result -1. So I'll have to use strncmp() or pointer walk but again it's not C++11 style.
There was a problem hiding this comment.
from doc:
value <0: Either the value of the first character that does not match is lower in the compared string, or all compared characters match but the compared string is shorter.
There was a problem hiding this comment.
Sigh, indeed. What a terrible API.
test/common/ssl/context_impl_test.cc
Outdated
| TEST_F(SslContextImplTest, TestURIMatch) { | ||
| EXPECT_TRUE(ContextImpl::uriMatch("spiffe://lyft.com/foo", "spiffe://lyft.com/foo")); | ||
| EXPECT_TRUE(ContextImpl::uriMatch("spiffe://lyft.com/*", "spiffe://lyft.com/foo")); | ||
| EXPECT_TRUE(ContextImpl::uriMatch("spiffe://lyft.com/*", "spiffe://lyft.com/")); |
There was a problem hiding this comment.
I'm not sure if this should pass (logically, not according to the code).
There was a problem hiding this comment.
why not? ;) the first case fall back to uriPattern == uri. For the rest two uri is compared as a buffer and the prefixes all agree.
There was a problem hiding this comment.
I only meant the last one, i.e. spiffe://lyft.com/ matches spiffe://lyft.com/*, which doesn't feel right.
There was a problem hiding this comment.
I see... I'll make this a false case
test/common/ssl/context_impl_test.cc
Outdated
| EXPECT_TRUE(ContextImpl::uriMatch("spiffe://lyft.com/foo", "spiffe://lyft.com/foo")); | ||
| EXPECT_TRUE(ContextImpl::uriMatch("spiffe://lyft.com/*", "spiffe://lyft.com/foo")); | ||
| EXPECT_TRUE(ContextImpl::uriMatch("spiffe://lyft.com/*", "spiffe://lyft.com/")); | ||
| EXPECT_TRUE(ContextImpl::uriMatch("spiffe://lyft.com/foo/*", "spiffe://lyft.com/foo/bar")); |
There was a problem hiding this comment.
Could you also add test for:
EXPECT_TRUE(ContextImpl::uriMatch("spiffe://lyft.com/*", "spiffe://lyft.com/foo/bar"));
source/common/ssl/context_impl.cc
Outdated
|
|
||
| bool ContextImpl::uriMatch(const std::string& uriPattern, const char* uri) { | ||
| size_t pattern_len = uriPattern.length(); | ||
| if (pattern_len > 1 && uriPattern[pattern_len - 1] == '*' && uriPattern[pattern_len - 2] == '/') { |
There was a problem hiding this comment.
Perhaps
if (pattern_len > 1 && uriPattern.substr(pattern_len - 2) == '/*') {
would be more readable?
There was a problem hiding this comment.
s/in the end/at the end/.
docs/configuration/listeners/ssl.rst
Outdated
There was a problem hiding this comment.
s/in the end/at the end/.
source/common/ssl/context_impl.h
Outdated
There was a problem hiding this comment.
Please remove space and perhaps replace this with the same example as in docs (i.e. foo.com/*).
There was a problem hiding this comment.
I tried foo.com/* but compiler gave me this weird error:
error: "/*" within comment [-Werror=comment]
There was a problem hiding this comment.
Just remove the example, then.
mattklein123
left a comment
There was a problem hiding this comment.
Small nit. LGTM pending @PiotrSikora +1
source/common/ssl/context_impl.cc
Outdated
| } | ||
|
|
||
| bool ContextImpl::uriMatch(const std::string& uriPattern, const char* uri) { | ||
| size_t pattern_len = uriPattern.length(); |
| *(optional, array)* An optional list of subject alt names. If specified, Envoy will verify | ||
| that the server certificate's subject alt name matches one of the specified values. | ||
| that the server certificate's subject alt name matches one of the specified values. The names | ||
| support wildcard at the end. For example, ``foo.com/*`` will match certificate with subject alt |
There was a problem hiding this comment.
Could you make it clear that this wildcard support is only for URI type SAN. And also for DNS type SAN, there's a wildcard support for "*.foo.com" style?
source/common/ssl/context_impl.cc
Outdated
| return verified; | ||
| } | ||
|
|
||
| bool ContextImpl::uriMatch(const std::string& uriPattern, const char* uri) { |
There was a problem hiding this comment.
nit: Maybe move this function after dNSNameMatch, to be consistent with the order in the .h file.
| @@ -209,7 +209,7 @@ bool ContextImpl::verifySubjectAltName(X509* cert, | |||
| ASN1_STRING* str = altname->d.uniformResourceIdentifier; | |||
| char* crt_san = reinterpret_cast<char*>(ASN1_STRING_data(str)); | |||
There was a problem hiding this comment.
It just occurred to me that there is ASN1_STRING_length(), which is O(1), so this could be changed to:
std::string crt_san(reinterpret_cast<char *>(ASN1_STRING_data(str)), ASN1_STRING_length(str));
and then we could compare strings only if crt_san.length() >= uriPattern.length() and use std::string::compare() again.
Sorry for not catching this yesterday!
There was a problem hiding this comment.
I'll fix this along with the problem mentioned below.
There was a problem hiding this comment.
a side question though, if we use string (const char* s, size_t n) then isn't it also O(n)? Since the constructor always copies the characters
There was a problem hiding this comment.
Yes, that is correct. Optimally, You would just pass char* and length to the function, then still use strncmp (or compare if you can). At this point though let's just get something working that everyone agrees with. :)
There was a problem hiding this comment.
cool, I'll stick to the current way then
There was a problem hiding this comment.
...it is. Feel free to ignore this.
Alternatively, you could use ASN1_STRING_length() and pass size_t to do the crt_san_len >= uriPattern.length() check and use std::string::compare().
There was a problem hiding this comment.
right. I'll pass size_t since it also makes DNS match easier.
source/common/ssl/context_impl.cc
Outdated
| return uriPattern == uri; | ||
| } | ||
|
|
||
| bool ContextImpl::dNSNameMatch(const std::string& dNSName, const char* pattern) { |
There was a problem hiding this comment.
Sorry to bring this up ... I feel like how this function is called (line 203 in context_impl.cc) is not right. It should be:
dNSNameMatch(dns_name, config_san)
config_san should be the pattern, not dns_name from the cert.
It makes sense to me that we support wildcard in config, not in certificates. Unfortunately I didn't notice this when I modified this function. This was unnoticed because we don't have integration tests, nor did we tell users this wildcard is supported.
@mattklein123 Could you confirm?
There was a problem hiding this comment.
@myidpt @mattklein123 I can fix this and update the doc. So the SAN field, whether it's URI or DNS, should never contain a wildcard; and we allow configs to optionally use wildcard to match multiple SAN values.
There was a problem hiding this comment.
Thank you very much. Please also make sure the function signature (parameter naming&order) aligns with the uriMatch function :)
| char* dns_name = reinterpret_cast<char*>(ASN1_STRING_data(str)); | ||
| for (auto& config_san : subject_alt_names) { | ||
| if (dNSNameMatch(config_san, dns_name)) { | ||
| if (dNSNameMatch(config_san, dns_name, static_cast<size_t>(ASN1_STRING_length(str)))) { |
There was a problem hiding this comment.
You probably want to save this to a local variable before the for loop.
There was a problem hiding this comment.
To avoid extra calls to ASN1_STRING_length() (since I don't think it will get inlined) and dereference of str.
| char* crt_san = reinterpret_cast<char*>(ASN1_STRING_data(str)); | ||
| for (auto& config_san : subject_alt_names) { | ||
| if (config_san.compare(crt_san) == 0) { | ||
| if (uriMatch(config_san, crt_san, static_cast<size_t>(ASN1_STRING_length(str)))) { |
| "ssl_context": { | ||
| "ca_cert_file": "{{ test_rundir }}/test/config/integration/certs/upstreamcacert.pem", | ||
| "verify_subject_alt_name": [ "foo.lyft.com" ] | ||
| "verify_subject_alt_name": [ "*.lyft.com" ] |
There was a problem hiding this comment.
I'd prefer if you add new test cases, so that we have both exact match and wildcard covered.
There was a problem hiding this comment.
I think unit tests already cover both cases. The purpose of integration tests should just be making sure that the overall flow works?
There was a problem hiding this comment.
I disagree, especially when most of the verification and SAN extraction is actually happening in 3rd-party library, which isn't involved at all in those unit tests.
There was a problem hiding this comment.
FWIW, I disagree also. Please keep the existing test and add a new one. Changing this code almost always breaks something / someone so the more coverage we have the better. (Hence why we are now at like 60 comments on a tiny PR).
|
@crazytan friendly ping to make the requested changes so that we can close this out. Thank you. |
|
Closing this for now. Please reopen when ready to finish. |
Resolves #1292. Handles the iOS portion of #1291, and the Android changes are being tracked there. Risk Level: Medium, new filter being added to the core chain Testing: Added in PR Docs Changes: N/A Signed-off-by: Michael Rebello <me@michaelrebello.com> Co-authored-by: Jose Nino <jnino@lyft.com> Signed-off-by: JP Simard <jp@jpsim.com>
Resolves #1292. Handles the iOS portion of #1291, and the Android changes are being tracked there. Risk Level: Medium, new filter being added to the core chain Testing: Added in PR Docs Changes: N/A Signed-off-by: Michael Rebello <me@michaelrebello.com> Co-authored-by: Jose Nino <jnino@lyft.com> Signed-off-by: JP Simard <jp@jpsim.com>
**Description** Fix response_format schema type with raw byte to avoid sorting behavior by `json/encode` Go library. Fixes #1298 --------- Signed-off-by: Xiaolin Lin <xlin158@bloomberg.net>
As discussed in #1272 , I made changes so that an optional character
*is allowed at the end of string inverify_subject_alt_nameoption. For example,spiffe://foo.com/*will match all names starting withspiffe://foo.com/. The wildcard is only allowed in the end of path, not host.