-
-
Notifications
You must be signed in to change notification settings - Fork 24
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
Enable Certificate Transparency with OkHTTP Update #2681
Conversation
@damagatchi retest this please |
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 I'm interpreting the current behavior correctly there are some changes we should make on these, but also satisfied with clarity in how it was determined that the behavior is different from what I'm interpreting.
} | ||
|
||
private val getInterceptor = certificateTransparencyInterceptor { | ||
logger = object : CTLogger { |
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.
What's the frequency of this request pattern? The example lists a failed log, but it looks like at a minimum we would also expect getting a log record (or multiple) for each channel negotiation, which I don't think makes Logger the right channel unless we're filtering for failure more specifically
Based on the noisiness of logging I've been trying to push us back from using the CommCare logs for any events which are "unexceptional" and which would occur with higher frequency than user sessions. IE: If something normal / expected happens once per login it's fine to log, but if it happens on "machine frequency" (IE: a background process is retrying over and over) we should push to LogCat rather than CommCare's logs. It's also good to log blocking failures (Since eventually a user recovers from them), we just don't want to be constantly filling our sumo allocations with that data indefinitely.
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.
Completely, indeed this would create a lot of noise as it's supposed to log the result of every negotiation. I will change it to only log failures (only once), which will allow us to know when someone gets blocked. However, I don't think in that situation we would receive anything from the device.
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.
Cool, thanks for confirming. It's totally possible we could get the logged data from a request later if, for example, someone is trying to trick your WIFI with a MITM temporarily but you switch to mobile internet because it's not working.
} | ||
|
||
// In case there are CT Interceptors already attached | ||
removeCTInterceptors(client) |
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.
I don't think this is possible, since the builder starts fresh each time in customizeRetrofitSetup()
, so I'd prefer to avoid the redundancy, and especially the class name reflection check. Did you test specifically to see if things were getting added multiple times without this?
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.
Yes, that's what I thought too but everything in the CommCareNetworkServiceGenerator
is static and so we get the same OkHttpClient.Builder
object with all the interceptors added previously. So this is just to remove the CT interceptors before the build()
method is called. Unless I'm missing something.
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.
Ah, got it.
That does seem tricky to deal with. I am a bit concerned about the structure of this approach being a straight string comparison, though.
Could we move this comparison to an instanceof
check instead? That would prevent future drift issues that require a tight coupling of that string and the name
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 can't because the CertificateTransparencyInterceptor
class is internal, but we can switch to an object comparison.
@@ -1183,4 +1182,8 @@ public AndroidPackageUtils getAndroidPackageUtils() { | |||
public boolean isNsdServicesEnabled() { | |||
return true; | |||
} | |||
|
|||
public void initCertificateTransparency() { | |||
CommCareNetworkServiceGenerator.customizeRetrofitSetup(new CTInterceptorConfig()); |
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.
Hm, are we sure this chains in this way? my read from the current code is that this may be overwriting this config rather than chaining it, and we may need a cleaner way to compose these two HttpBuilderConfig classes. It should be easy too, since they both act on the builder, we should just be able to make a composite that runs multiple.
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.
@ctsims I had the opportunity to retest this and confirm my understanding. So, this is not supposed to impact any of the existing configuration of the OkHttpClient object such as the SSLSocketFactory, it just adds a new network interceptor and it sort of operates at a higher level. For instance, we can set a TrustManager for the Network Interceptor and it shouldn't override the TrustManager of the OkHttpClient. But I do like the idea of having a single entry point for these config classes.
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.
+1 to Clayton's comments.
a new Developers option called Certificate Transparency has been added to allow the users to disable the feature without having to update the app.
I would not expect users to interact with Developers Option at all and if we expect users to be able to turn this off - should we have it somewhere in Advanced Settings sections instead ? But I am also curious why do we want the user to have the ability to turn this off if an app specfically enforces it for it's users.
This is a like a fail-safe mechanism, to be used if, for some reason, CT stops validating the certificate. I imagine that this can happen when the certificate expires, or it's renewed with a different CA and it takes some time to propagate, or even, due to a failure on CT infra side, but I'm just guessing. I don't know for sure in which situations this is applicable but I think it's important to have a local mechanism to disabled it. Also because in that situation the device will not be able to communicate with HQ and so, updating the app to switch off the feature won't be possible. As per where this should live, I'm open to suggestions, I think Advanced Settings
is a good option, but wouldn't it be too exposed? But I guess it wouldn't be a problem for a user to accidentally enable it.
return client | ||
} | ||
|
||
private val getInterceptor = certificateTransparencyInterceptor { |
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.
maybe lazy init this to make sure we are not initialising the interceptor when the certificate transparency is disabled.
@damagatchi retest this please |
1 similar comment
@damagatchi retest this please |
There are a few scenarios to consider, but the three most relevant are
The first is the main one that requires a local option, since otherwise they wouldn't be able to download the app which would configure their device to not use it. I would mildly prefer to hide it behind developer options, simply because the need to use it is hypothetical and I don't want to introduce it unless there's a real demonstrated need or use case. If we do determine that for some reason CT works less well than we expect I could see moving it to a normal user setting instead. |
@damagatchi retest this please |
I will take it back to |
@damagatchi retest this please |
d18f18f
to
d851810
Compare
@damagatchi retest this please |
@damagatchi retest this please |
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.
Some minor code feedback but looks good to go as it is.
logger = object : CTLogger { | ||
override fun log(host: String, result: VerificationResult) { | ||
if (result is VerificationResult.Failure && !previousRequestFailed) { | ||
Logger.log(LogTypes.TYPE_NETWORK, "$host -> $result") |
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 we add an additional message to the log - Logger.log(LogTypes.TYPE_NETWORK, "Certification verification failed: $host -> $result")
Logger.log(LogTypes.TYPE_NETWORK, "$host -> $result") | ||
previousRequestFailed = true | ||
} else if (result is VerificationResult.Success && previousRequestFailed) { | ||
previousRequestFailed = false |
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 assignment can be moved out of if else block - previousRequestFailed = result is VerificationResult.Success
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.
I think the idea is to compare it against VerificationResult.Failure
ctInterceptorConfig = new CTInterceptorConfig(); | ||
isrgCertConfig = new ISRGCertConfig(); |
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.
Lets initialise them in performCustomConfig
itself. That way we can make sure they are only initialised if needed. For ex. we don't need to init ISRGCertConfig
if if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1) {
is not 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.
good point
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.
on another thought, we actually don't need to instantiate these
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 you say more to what you are thinking ?
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.
Considering also that all the CommCareNetworkServiceGenerator
content is static we don't actually need instances of CTInterceptorConfig
and ISRGCertConfig
to apply these custom configurations, right?
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.
@shubham1g5 in concrete terms, this is what I'm thinking.
|
||
// This is part of the CommCare app initialization because it needs to be applied during | ||
// app initialization, update and when switching the seated app | ||
customiseOkHttp(); |
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.
@shubham1g5 I thought I had included in the initial PR. Basically, because the Certificate Transparency is a CommCare app level configuration, we need to be able to apply it in case it changes between app updates and when the user switches the seated app.
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 ISRGCert needs to be attached on application start up as before otherwise users on some devices might not be able to download CC app itself.
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.
@shubham1g5 true, when CommCare is installed there is not CC app to initialize and the configuration won't applied. I think consolidating configurations of different cycles/types is probably a stretch, but we can continue applying the configuration during app startup, just to cover all the scenarios.
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.
@damagatchi retest this please |
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.
Couple questions -
-
How have you tested this change ? Can we specify it in the Safety story for PR
-
This setting needs to be QA'ed as part of regular releases and therefore we should have a QA note with QA note label set.
|
||
// This is part of the CommCare app initialization because it needs to be applied during | ||
// app initialization, update and when switching the seated app | ||
customiseOkHttp(); |
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 ISRGCert needs to be attached on application start up as before otherwise users on some devices might not be able to download CC app itself.
Yes, I have tested this a few time and in different use cases.
The label has been added and agree, this needs to be QA'ed. |
@damagatchi retest this please |
Summary
This PR addresses a vulnerability flagged by the ABDM auditors, according to which an attacker is able to use open source tools to intercepts requests and perform MITM attacks. The initial recommendation was to adopt Certificate Pinning but after some investigation we opted for Certificate Transparency (CT), considering the restrictions and maintenance burden related with pinning certificates. Certificate Transparency verifies the validity of the host's SSL certificate by checking the existence of Signed Certificate Timestamps (SCTs). SCTs are basically a confirmation that the certificate was submitted to a CT log by the Certificate Authorities (CA) when a new certificate is issued.
Feature Flag
This feature is behind a CommCare custom property called
cc-enable-certificate-transparency
Product Description
When the verification fails, meaning there are no SCTs that accompany the Certificate, the connection is rejected and an exception such as the one below is raised.
Considering that this feature is supposed to be enabled at the CommCare App level, a new Advanced menu option called
Certificate Transparency
has been added to allow the users to disable the feature without having to update the app.Safety Assurance
Cross-request: dimagi/commcare-core#1311