-
Notifications
You must be signed in to change notification settings - Fork 3.9k
12326 :: DNS SAN matching does not handle wildcards in split patterns that Envoy does #12345
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
base: master
Are you sure you want to change the base?
Conversation
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.
Left comments.
return labelWildcardMatch(certLabels[0], sanLabels[0]); | ||
} | ||
|
||
private static boolean labelWildcardMatch(String certLabel, String sanLabel) { |
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.
Rename certLabel to pattern and sanLabel to dnsLabel like in Envoy code, it is more clear that 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.
Addressed the review comments
int starIndex = sanLabel.indexOf('*'); | ||
String prefix = sanLabel.substring(0, starIndex); | ||
String suffix = sanLabel.substring(starIndex + 1); | ||
if (certLabel.length() < prefix.length() + suffix.length()) { |
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 better said as certLabel.length() <= sanLabel.length() + 1 like in Envoy code. The +1 is for * that stands for 0 or more characters. Combine with below condition in the return statement
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.
Thanks @kannanjgithub for the review , I have addressed the review comments.
|
||
public static boolean verifyDnsNameWildcard( | ||
String altNameFromCert, String sanToVerify, boolean ignoreCase) { | ||
if (Strings.isNullOrEmpty(altNameFromCert) || Strings.isNullOrEmpty(sanToVerify)) { |
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.
We don't need this check since the logic can already handle empty strings, and null won't be passed for sanToVerify here. altNameFromCert wont' be null.
Instead have the trivial check for "*" like Envoy does
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.
Addressed the review comments
? sanToVerifySuffix.toLowerCase(Locale.ROOT) | ||
: sanToVerifySuffix).contains("*")) { | ||
return verifyDnsNameWildcard(altNameFromCert, sanToVerifySuffix , ignoreCase); | ||
} |
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.
Looking at the envoy code here it is not doing wildcard matching for prefix, suffix, and contains and for safe regex it is compiling the pattern as regex. gRPC code is already doing the same for these 3 cases, so only for exact DNS match we need to have the changes for handling split patterns and *.
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.
Addressed the review comments
@@ -0,0 +1,24 @@ | |||
-----BEGIN CERTIFICATE----- | |||
MIID7zCCAtegAwIBAgIUCs5j4C2KXgCRVFa48kc5TYRS1JswDQYJKoZIhvcNAQEL | |||
BQAwGTEXMBUGA1UEAwwOTXkgSW50ZXJuYWwgQ0EwHhcNMjUwOTA4MTI0NTQyWhcN |
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 need to create any new certificate, we can work with
testing/src/main/resources/certs/server0.pem and server1.pem themselves. server0.pem is issued to *.test.google.com.au server1.pem is issued to the names
*.test.google.fr, waterzooi.test.google.be, *.test.youtube.com, IP Address:192.168.1.3
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.
Addressed the review comments and used the existing server1.pem for the junit's
@@ -147,6 +147,11 @@ private static boolean verifyDnsNameExact( | |||
if (Strings.isNullOrEmpty(sanToVerifyExact)) { | |||
return false; | |||
} | |||
if ((ignoreCase | |||
? sanToVerifyExact.toLowerCase(Locale.ROOT) |
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.
ignoreCase ? sanToVerifyExact.toLowerCase(Locale.ROOT)
is redundant
as "*"
is case-insensitive
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.
Addressed the review comments
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.
Doesn't match envoy pattern magic logic.
if ((ignoreCase | ||
? sanToVerifyExact.toLowerCase(Locale.ROOT) | ||
: sanToVerifyExact).contains("*")) { | ||
return verifyDnsNameWildcard(altNameFromCert, sanToVerifyExact , ignoreCase); |
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.
nit: Remove space before the comma.
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.
Noted
public static boolean verifyDnsNameWildcard( | ||
String altNameFromCert, String sanToVerify, boolean ignoreCase) { | ||
String[] pattern = (ignoreCase ? altNameFromCert.toLowerCase(Locale.ROOT) : altNameFromCert) | ||
.split("\\.", -1); |
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.
We need only 1 split, check envoy code: https://github.com/envoyproxy/envoy/blob/d8c394ca5aecd237086f6f89933fcfced582386e/source/common/tls/utility.cc#L81
-1 for split allows any number of splits.
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.
Thanks @kannanjgithub for the review , I have updated this logic as per the envoy code and now the we have the 1 split by delimiter and comparing the left and right labels.
String prefix = dnsLabel.substring(0, starIndex); | ||
String suffix = dnsLabel.substring(starIndex + 1); | ||
return pattern.length() >= (dnsLabel.length() - 1) | ||
&& pattern.startsWith(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.
We should only look for * in pattern, not in dnsLabel. Check https://github.com/envoyproxy/envoy/blob/d8c394ca5aecd237086f6f89933fcfced582386e/source/common/tls/utility.cc#L64
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.
Thanks @kannanjgithub for the suggestions, I have strictly followed the Envoy logic in latest changes, excluding the exact match check, as it is handled in verifyDnsNameExact, and have updated the changes accordingly.
if (lower_case_dns_name == lower_case_pattern) {
return true;
}
} | ||
|
||
private static String[] splitAtFirstDelimiter(String s) { | ||
int index = s.indexOf("."); |
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.
'.'
- as character
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.
Noted.
private static String[] splitAtFirstDelimiter(String s) { | ||
int index = s.indexOf("."); | ||
if (index == -1) { | ||
return new String[]{s}; |
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 contents of the single element array is not used, in terms of performance I would return null
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.
@panchenko , I agree that the single-element array isn't used in most cases. However, returning it prevents potential NullPointerExceptions, allowing the caller to use splitAtFirstDelimiter safely without needing a null check. Given this, I believe the current approach is more robust and maintainable. I would appreciate your thoughts before proceeding with any changes.
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.
@vimanikag So the method should be annotated with the @Nullable
annotation.
If returning null then slightly shorter code here and also the check if no delimiter found.
In general, what should be returned if delimiter is not found varies by use case, in some situations it could be["", s]
or the other way around, so returning null to indicate that there are no 2 elements it not a bad idea.
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.
Thanks, @panchenko , for sharing your thoughts. I've addressed the review comments.
if (pattern.length() == 1 && pattern.charAt(0) == glob) { | ||
return true; | ||
} | ||
if (pattern.chars().filter(ch -> ch == glob).count() == 1) { |
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.
in terms of performance it can be
int globIndex = pattern.indexOf(glob);
if (pattern.indexOf(glob, globIndex + 1) == -1) {
return dnsLabel.length() >= pattern.length() - 1
&& dnsLabel.startsWith(pattern.substring(0, globIndex))
&& dnsLabel.endsWith(pattern.substring(globIndex + 1));
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.
Thanks @panchenko for the suggestions, I have addressed the review comments
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.
Changes needed.
|
||
private static boolean labelWildcardMatch(String dnsLabel, String pattern) { | ||
final char glob = '*'; | ||
if (pattern.length() == 1 && pattern.charAt(0) == glob) { |
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.
Just do if (pattern.equals("*"))
. Envoy does in a C++ typical way using char comparison, we don't need to do that in Java. String.equals() is highly optimized in Java and we don't need to do it ourselves.
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.
Noted.
} | ||
|
||
private static boolean labelWildcardMatch(String dnsLabel, String pattern) { | ||
final char glob = '*'; |
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 add the comment
// Check the special case of a single * pattern, as it's common.
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.
Noted.
assertFalse(verifyDnsNameWildcard("", "*lyft.com", true)); | ||
assertFalse(verifyDnsNameWildcard("lyft.com", "", true)); | ||
assertFalse(verifyDnsNameWildcard("xn--lyft.com", "*.xn--lyft.com", true)); | ||
} |
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 adds no value, we deny if xn-- is present in pattern[0], the failure here is due to other reasons, and is misleading. Change it to a negative test case with xn-- in pattern[0].
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.
@kannanjgithub , I have not addressed this review comment yet because our current certificates do not include this pattern. I will address this in a future commit once we finalize the new patterns.
} | ||
|
||
@Test | ||
public void testVerifyDnsNameWildcard() { |
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.
verifyDnsNameWildcard should never have been a public method, it should only be a private method and it is easily testable via the public method. Have a helper method take the arguments
- Pattern
- Certificate file to use - server0 or server1
- Ignore case or not
- Assert true or false
And have a single @test that calls this helper method with all the test data you have.
This can be a junit parameterized test but I'm not suggesting that because I have parameterized tests upcoming for a different feature with a different set of parameters.
You will have certificate patterns like "l-lots-of-stuff-ft.com", ".lyft.com", ""
that are not testable because our test certificates don't have them. Some of these such as the above 3 SAN names seem redundant to test. If there are others that are unable to be tested via existing certs and are non-redundant please list them and we can think whether we need to create test certs with those names.
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.
@kannanjgithub , I have partially addressed your review comment and will complete the remaining work in a future commit once we finalize the new patterns for our certificates.
To assist with this, I have compiled a list of valid and invalid patterns below. Most of these have already been included in my initial commit (919285c). Please review these patterns and confirm if the attached certificates are acceptable. If you approve, I will ensure they are included in the next commit with the new certificates.
I have attached a document link/attachment as few patterns are altered in GitHub comments :
https://docs.google.com/document/d/1O0hzmTdwTEO8znjJGje9plXm13EWwGpE2xqdD5-vnpM/edit?usp=sharing
} | ||
|
||
@Test | ||
public void testVerifyDnsNameWildcard() { |
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.
-
Add a test for exact match without wildcard by setting
StringMatcher.newBuilder() .setExact("waterzooi.test.google.be")
This SAN name is present in the server1.pem test certificate. -
Add a failure case with
.setExact("*.TEST.YOUTUBE.com") .setIgnoreCase(false)
and assert that the call fails. -
Add a test for String matcher exact pattern with * and without any '.' in it is it fails validation because split patterns will return null.
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.
Noted.
Fixes : #12326