Skip to content

ssl: support wildcard matching for verify_subject_alt_name#1298

Closed
crazytan wants to merge 6 commits intoenvoyproxy:masterfrom
crazytan:master
Closed

ssl: support wildcard matching for verify_subject_alt_name#1298
crazytan wants to merge 6 commits intoenvoyproxy:masterfrom
crazytan:master

Conversation

@crazytan
Copy link

As discussed in #1272 , I made changes so that an optional character * is allowed at the end of string in verify_subject_alt_name option. For example, spiffe://foo.com/* will match all names starting with spiffe://foo.com/. The wildcard is only allowed in the end of path, not host.

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Beyond doc/format failures, small perf comments. @myidpt could you take a quick look?

Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

agree that the code is not very efficient. Will rewrite so that it only takes one walk through both strings.

@mattklein123 mattklein123 requested a review from myidpt July 21, 2017 00:24
Copy link
Member

Choose a reason for hiding this comment

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

I don't think you can always safely dereference *(p - 1). Please add test for this while fixing.

Copy link
Contributor

Choose a reason for hiding this comment

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

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)

Copy link
Member

@mattklein123 mattklein123 Jul 24, 2017

Choose a reason for hiding this comment

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

Yes that would be much better. :)

Copy link
Author

Choose a reason for hiding this comment

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

I'm confused but std::string::compare is what I was using. So should I use pointer walk through or string::compare?

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

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.

@mattklein123
Copy link
Member

@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.

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;
Copy link
Member

Choose a reason for hiding this comment

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

I think you will need to use strncmp() here since IIRC compare will go off end of string. (See ASAN failure).

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sigh, indeed. What a terrible API.

Copy link
Author

Choose a reason for hiding this comment

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

I'll use strncmp() then.

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/"));
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure if this should pass (logically, not according to the code).

Copy link
Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

I only meant the last one, i.e. spiffe://lyft.com/ matches spiffe://lyft.com/*, which doesn't feel right.

Copy link
Author

Choose a reason for hiding this comment

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

I see... I'll make this a false case

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"));
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you also add test for:

EXPECT_TRUE(ContextImpl::uriMatch("spiffe://lyft.com/*", "spiffe://lyft.com/foo/bar"));

Copy link
Author

Choose a reason for hiding this comment

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

will add


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] == '/') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps

if (pattern_len > 1 && uriPattern.substr(pattern_len - 2) == '/*') {

would be more readable?

Copy link
Author

Choose a reason for hiding this comment

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

agree

Copy link
Contributor

Choose a reason for hiding this comment

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

s/in the end/at the end/.

Copy link
Contributor

Choose a reason for hiding this comment

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

s/in the end/at the end/.

Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove space and perhaps replace this with the same example as in docs (i.e. foo.com/*).

Copy link
Author

Choose a reason for hiding this comment

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

I tried foo.com/* but compiler gave me this weird error:

error: "/*" within comment [-Werror=comment]

Copy link
Contributor

Choose a reason for hiding this comment

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

Just remove the example, then.

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Small nit. LGTM pending @PiotrSikora +1

}

bool ContextImpl::uriMatch(const std::string& uriPattern, const char* uri) {
size_t pattern_len = uriPattern.length();
Copy link
Member

Choose a reason for hiding this comment

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

nit: const size_t

*(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
Copy link
Contributor

Choose a reason for hiding this comment

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

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?

return verified;
}

bool ContextImpl::uriMatch(const std::string& uriPattern, const char* uri) {
Copy link
Contributor

Choose a reason for hiding this comment

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

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));
Copy link
Contributor

Choose a reason for hiding this comment

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

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!

Copy link
Author

Choose a reason for hiding this comment

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

I'll fix this along with the problem mentioned below.

Copy link
Author

@crazytan crazytan Jul 26, 2017

Choose a reason for hiding this comment

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

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

Copy link
Member

Choose a reason for hiding this comment

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

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. :)

Copy link
Author

Choose a reason for hiding this comment

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

cool, I'll stick to the current way then

Copy link
Contributor

Choose a reason for hiding this comment

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

...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().

Copy link
Author

Choose a reason for hiding this comment

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

right. I'll pass size_t since it also makes DNS match easier.

return uriPattern == uri;
}

bool ContextImpl::dNSNameMatch(const std::string& dNSName, const char* pattern) {
Copy link
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Member

Choose a reason for hiding this comment

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

@myidpt yes that make sense to me. Can we fix?

Copy link
Author

@crazytan crazytan Jul 26, 2017

Choose a reason for hiding this comment

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

@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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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)))) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You probably want to save this to a local variable before the for loop.

Copy link
Author

Choose a reason for hiding this comment

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

why?

Copy link
Contributor

Choose a reason for hiding this comment

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

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)))) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

"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" ]
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer if you add new test cases, so that we have both exact match and wildcard covered.

Copy link
Author

Choose a reason for hiding this comment

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

I think unit tests already cover both cases. The purpose of integration tests should just be making sure that the overall flow works?

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Member

Choose a reason for hiding this comment

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

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).

@mattklein123
Copy link
Member

@crazytan friendly ping to make the requested changes so that we can close this out. Thank you.

@mattklein123
Copy link
Member

Closing this for now. Please reopen when ready to finish.

jpsim pushed a commit that referenced this pull request Nov 28, 2022
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>
jpsim pushed a commit that referenced this pull request Nov 29, 2022
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>
mathetake pushed a commit that referenced this pull request Mar 3, 2026
**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>
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.

4 participants