Skip to content

Commit

Permalink
feat(webconnectivityqa): import misconfigured-TLS test cases (#1243)
Browse files Browse the repository at this point in the history
## Checklist

- [x] I have read the [contribution
guidelines](https://github.com/ooni/probe-cli/blob/master/CONTRIBUTING.md)
- [x] reference issue for this pull request:
ooni/probe#1803
- [x] if you changed anything related to how experiments work and you
need to reflect these changes in the ooni/spec repository, please link
to the related ooni/spec pull request: N/A
- [x] if you changed code inside an experiment, make sure you bump its
version number: N/A

## Description

This diff imports the misconfigured-TLS test cases of the
QA/webconnectivity.py test suite in webconnectivityqa.

The only QA/webconnectivity.py test case we're not merging is the one
about self-signed certificate, which are equivalent enough to an unknown
root certificate that it seems unimportant to merge them.

In other word, we have basically finished rewriting Jafar. Now it will
be time to drop Jafar. 😅
  • Loading branch information
bassosimone committed Sep 6, 2023
1 parent e14895a commit 18f9dcb
Show file tree
Hide file tree
Showing 10 changed files with 375 additions and 158 deletions.
152 changes: 0 additions & 152 deletions QA/webconnectivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,80 +47,6 @@ def assert_status_flags_are(ooni_exe, tk, desired):
assert tk["x_status"] == desired


def webconnectivity_https_expired_certificate(ooni_exe, outfile):
"""Test case where the domain's certificate is expired"""
args = []
tk = execute_jafar_and_return_validated_test_keys(
ooni_exe,
outfile,
"-i https://expired.badssl.com/ web_connectivity",
"webconnectivity_https_expired_certificate",
args,
)
assert tk["dns_experiment_failure"] == None
assert tk["dns_consistency"] == "consistent"
assert tk["control_failure"] == None
if "miniooni" in ooni_exe:
assert tk["http_experiment_failure"] == "ssl_invalid_certificate"
else:
assert "certificate verify failed" in tk["http_experiment_failure"]
assert tk["body_length_match"] == None
assert tk["body_proportion"] == 0
assert tk["status_code_match"] == None
assert tk["headers_match"] == None
assert tk["title_match"] == None
# The following strikes me as a measurement_kit bug. We are saying
# that all is good with a domain where actually we don't know why the
# control is failed and that is clearly not accessible according to
# our measurement of the domain (certificate expired).
#
# See <https://github.com/ooni/probe-engine/issues/858>.
if "miniooni" in ooni_exe:
assert tk["blocking"] == None
assert tk["accessible"] == None
else:
assert tk["blocking"] == False
assert tk["accessible"] == True
assert_status_flags_are(ooni_exe, tk, 16)


def webconnectivity_https_wrong_host(ooni_exe, outfile):
"""Test case where the hostname is wrong for the certificate"""
args = []
tk = execute_jafar_and_return_validated_test_keys(
ooni_exe,
outfile,
"-i https://wrong.host.badssl.com/ web_connectivity",
"webconnectivity_https_wrong_host",
args,
)
assert tk["dns_experiment_failure"] == None
assert tk["dns_consistency"] == "consistent"
assert tk["control_failure"] == None
if "miniooni" in ooni_exe:
assert tk["http_experiment_failure"] == "ssl_invalid_hostname"
else:
assert "certificate verify failed" in tk["http_experiment_failure"]
assert tk["body_length_match"] == None
assert tk["body_proportion"] == 0
assert tk["status_code_match"] == None
assert tk["headers_match"] == None
assert tk["title_match"] == None
# The following strikes me as a measurement_kit bug. We are saying
# that all is good with a domain where actually we don't know why the
# control is failed and that is clearly not accessible according to
# our measurement of the domain (wrong host for certificate).
#
# See <https://github.com/ooni/probe-engine/issues/858>.
if "miniooni" in ooni_exe:
assert tk["blocking"] == None
assert tk["accessible"] == None
else:
assert tk["blocking"] == False
assert tk["accessible"] == True
assert_status_flags_are(ooni_exe, tk, 16)


def webconnectivity_https_self_signed(ooni_exe, outfile):
"""Test case where the certificate is self signed"""
args = []
Expand Down Expand Up @@ -158,91 +84,13 @@ def webconnectivity_https_self_signed(ooni_exe, outfile):
assert_status_flags_are(ooni_exe, tk, 16)


def webconnectivity_https_untrusted_root(ooni_exe, outfile):
"""Test case where the certificate has an untrusted root"""
args = []
tk = execute_jafar_and_return_validated_test_keys(
ooni_exe,
outfile,
"-i https://untrusted-root.badssl.com/ web_connectivity",
"webconnectivity_https_untrusted_root",
args,
)
assert tk["dns_experiment_failure"] == None
assert tk["dns_consistency"] == "consistent"
assert tk["control_failure"] == None
if "miniooni" in ooni_exe:
assert tk["http_experiment_failure"] == "ssl_unknown_authority"
else:
assert "certificate verify failed" in tk["http_experiment_failure"]
assert tk["body_length_match"] == None
assert tk["body_proportion"] == 0
assert tk["status_code_match"] == None
assert tk["headers_match"] == None
assert tk["title_match"] == None
# The following strikes me as a measurement_kit bug. We are saying
# that all is good with a domain where actually we don't know why the
# control is failed and that is clearly not accessible according to
# our measurement of the domain (untrusted root certificate).
#
# See <https://github.com/ooni/probe-engine/issues/858>.
if "miniooni" in ooni_exe:
assert tk["blocking"] == None
assert tk["accessible"] == None
else:
assert tk["blocking"] == False
assert tk["accessible"] == True
assert_status_flags_are(ooni_exe, tk, 16)


def webconnectivity_https_unknown_authority_with_inconsistent_dns(ooni_exe, outfile):
"""Test case where the DNS is sending us towards a website where
we're served an invalid certificate"""
args = [
"-iptables-hijack-dns-to",
"127.0.0.1:53",
"-dns-proxy-hijack",
"example.org",
"-bad-proxy-address-tls",
"127.0.0.1:443",
"-tls-proxy-address",
"127.0.0.1:4114",
]
tk = execute_jafar_and_return_validated_test_keys(
ooni_exe,
outfile,
"-i https://example.org/ web_connectivity",
"webconnectivity_https_unknown_authority_with_inconsistent_dns",
args,
)
assert tk["dns_experiment_failure"] == None
assert tk["dns_consistency"] == "inconsistent"
assert tk["control_failure"] == None
if "miniooni" in ooni_exe:
assert tk["http_experiment_failure"] == "ssl_unknown_authority"
else:
assert "certificate verify failed" in tk["http_experiment_failure"]
assert tk["body_length_match"] == None
assert tk["body_proportion"] == 0
assert tk["status_code_match"] == None
assert tk["headers_match"] == None
assert tk["title_match"] == None
assert tk["blocking"] == "dns"
assert tk["accessible"] == False
assert_status_flags_are(ooni_exe, tk, 9248)


def main():
if len(sys.argv) != 2:
sys.exit("usage: %s /path/to/ooniprobelegacy-like/binary" % sys.argv[0])
outfile = "webconnectivity.jsonl"
ooni_exe = sys.argv[1]
tests = [
webconnectivity_https_expired_certificate,
webconnectivity_https_wrong_host,
webconnectivity_https_self_signed,
webconnectivity_https_untrusted_root,
webconnectivity_https_unknown_authority_with_inconsistent_dns,
]
for test in tests:
test(ooni_exe, outfile)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ require (
github.com/mitchellh/go-wordwrap v1.0.1
github.com/montanaflynn/stats v0.7.1
github.com/ooni/go-libtor v1.1.8
github.com/ooni/netem v0.0.0-20230905233956-4c9ebf9611c6
github.com/ooni/netem v0.0.0-20230906091637-85d962536ff3
github.com/ooni/oocrypto v0.5.3
github.com/ooni/oohttp v0.6.3
github.com/ooni/probe-assets v0.18.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -483,8 +483,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
github.com/ooni/go-libtor v1.1.8 h1:Wo3V3DVTxl5vZdxtQakqYP+DAHx7pPtAFSl1bnAa08w=
github.com/ooni/go-libtor v1.1.8/go.mod h1:q1YyLwRD9GeMyeerVvwc0vJ2YgwDLTp2bdVcrh/JXyI=
github.com/ooni/netem v0.0.0-20230905233956-4c9ebf9611c6 h1:0GHOnir3Dy4BIsoYTKPxPa3ixNOBxx1VSWsV9qxCfN8=
github.com/ooni/netem v0.0.0-20230905233956-4c9ebf9611c6/go.mod h1:3LJOzTIu2O4ADDJN2ILG4ViJOqyH/u9fKY8QT2Rma8Y=
github.com/ooni/netem v0.0.0-20230906091637-85d962536ff3 h1:zpTbzNzpo00cKbjLLnWMKjZeGLdoNC81vMiBDiur7NU=
github.com/ooni/netem v0.0.0-20230906091637-85d962536ff3/go.mod h1:3LJOzTIu2O4ADDJN2ILG4ViJOqyH/u9fKY8QT2Rma8Y=
github.com/ooni/oocrypto v0.5.3 h1:CAb0Ze6q/EWD1PRGl9KqpzMfkut4O3XMaiKYsyxrWOs=
github.com/ooni/oocrypto v0.5.3/go.mod h1:HjEQ5pQBl6btcWgAsKKq1tFo8CfBrZu63C/vPAUGIDk=
github.com/ooni/oohttp v0.6.3 h1:MHydpeAPU/LSDSI/hIFJwZm4afBhd2Yo+rNxxFdeMCY=
Expand Down
101 changes: 101 additions & 0 deletions internal/experiment/webconnectivityqa/badssl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package webconnectivityqa

import (
"github.com/ooni/netem"
"github.com/ooni/probe-cli/v3/internal/netemx"
)

// Sometimes people we measure the websites of let their certificates expire and
// we want to be confident about correctly measuring this condition
func badSSLWithExpiredCertificate() *TestCase {
return &TestCase{
Name: "badSSLWithExpiredCertificate",
Flags: TestCaseFlagNoLTE, // LTE flags it correctly but let's focus on v0.4 for now
Input: "https://expired.badssl.com/",
Configure: func(env *netemx.QAEnv) {
// nothing
},
ExpectErr: false,
ExpectTestKeys: &testKeys{
DNSConsistency: "consistent",
HTTPExperimentFailure: "ssl_invalid_certificate",
XStatus: 16, // StatusAnomalyControlFailure
XNullNullFlags: 4, // analysisFlagNullNullTLSMisconfigured
Accessible: nil,
Blocking: nil,
},
}
}

// Sometimes people we measure the websites of misconfigured the certificate names and
// we want to be confident about correctly measuring this condition
func badSSLWithWrongServerName() *TestCase {
return &TestCase{
Name: "badSSLWithWrongServerName",
Flags: TestCaseFlagNoLTE, // LTE flags it correctly but let's focus on v0.4 for now
Input: "https://wrong.host.badssl.com/",
Configure: func(env *netemx.QAEnv) {
// nothing
},
ExpectErr: false,
ExpectTestKeys: &testKeys{
DNSConsistency: "consistent",
HTTPExperimentFailure: "ssl_invalid_hostname",
XStatus: 16, // StatusAnomalyControlFailure
XNullNullFlags: 4, // analysisFlagNullNullTLSMisconfigured
Accessible: nil,
Blocking: nil,
},
}
}

// Let's be sure we correctly flag a website using an unknown-to-us authority.
func badSSLWithUnknownAuthorityWithConsistentDNS() *TestCase {
return &TestCase{
Name: "badSSLWithUnknownAuthorityWithConsistentDNS",
Flags: TestCaseFlagNoLTE, // LTE flags it correctly but let's focus on v0.4 for now
Input: "https://untrusted-root.badssl.com/",
Configure: func(env *netemx.QAEnv) {
// nothing
},
ExpectErr: false,
ExpectTestKeys: &testKeys{
DNSConsistency: "consistent",
HTTPExperimentFailure: "ssl_unknown_authority",
XStatus: 16, // StatusAnomalyControlFailure
XNullNullFlags: 4, // analysisFlagNullNullTLSMisconfigured
Accessible: nil,
Blocking: nil,
},
}
}

// This test case models when we're redirected to a blockpage website using a custom CA.
func badSSLWithUnknownAuthorityWithInconsistentDNS() *TestCase {
return &TestCase{
Name: "badSSLWithUnknownAuthorityWithInconsistentDNS",
Flags: 0,
Input: "https://www.example.com/",
Configure: func(env *netemx.QAEnv) {

// add DPI rule to force all the cleartext DNS queries to
// point the client to used the ISPProxyAddress
env.DPIEngine().AddRule(&netem.DPISpoofDNSResponse{
Addresses: []string{netemx.AddressBadSSLCom},
Logger: env.Logger(),
Domain: "www.example.com",
})

},
ExpectErr: false,
ExpectTestKeys: &testKeys{
DNSConsistency: "inconsistent",
HTTPExperimentFailure: "ssl_unknown_authority",
XStatus: 9248, // StatusExperimentHTTP | StatusAnomalyTLSHandshake | StatusAnomalyDNS
XDNSFlags: 4, // AnalysisDNSUnexpectedAddrs
XBlockingFlags: 33, // analysisFlagSuccess | analysisFlagDNSBlocking
Accessible: false,
Blocking: "dns",
},
}
}
51 changes: 51 additions & 0 deletions internal/experiment/webconnectivityqa/badssl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package webconnectivityqa

import (
"net/http"
"testing"

"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/netemx"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/runtimex"
)

func TestBadSSLConditions(t *testing.T) {
type testCaseConfig struct {
expectedErr string
testCase *TestCase
}

testcases := []*testCaseConfig{{
expectedErr: "ssl_unknown_authority",
testCase: badSSLWithUnknownAuthorityWithConsistentDNS(),
}, {
expectedErr: "ssl_invalid_certificate",
testCase: badSSLWithExpiredCertificate(),
}, {
expectedErr: "ssl_invalid_hostname",
testCase: badSSLWithWrongServerName(),
}, {
expectedErr: "ssl_unknown_authority",
testCase: badSSLWithUnknownAuthorityWithInconsistentDNS(),
}}

for _, tc := range testcases {
t.Run(tc.testCase.Name, func(t *testing.T) {
env := netemx.MustNewScenario(netemx.InternetScenario)
tc.testCase.Configure(env)

env.Do(func() {
client := netxlite.NewHTTPClientStdlib(log.Log)
req := runtimex.Try1(http.NewRequest("GET", tc.testCase.Input, nil))
resp, err := client.Do(req)
if err == nil || err.Error() != tc.expectedErr {
t.Fatal("unexpected err", err)
}
if resp != nil {
t.Fatal("expected nil resp")
}
})
})
}
}
5 changes: 5 additions & 0 deletions internal/experiment/webconnectivityqa/testcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ type TestCase struct {
// AllTestCases returns all the defined test cases.
func AllTestCases() []*TestCase {
return []*TestCase{
badSSLWithUnknownAuthorityWithConsistentDNS(),
badSSLWithExpiredCertificate(),
badSSLWithWrongServerName(),
badSSLWithUnknownAuthorityWithInconsistentDNS(),

controlFailureWithSuccessfulHTTPWebsite(),
controlFailureWithSuccessfulHTTPSWebsite(),

Expand Down
6 changes: 6 additions & 0 deletions internal/netemx/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,9 @@ const AddressPublicBlockpage = "83.224.65.41"

// ISPProxyAddress is the IP address of the ISP's HTTP transparent proxy.
const ISPProxyAddress = "130.192.182.17"

// AddressBitly is the IP address of bitly.com.
const AddressBitly = "67.199.248.11"

// AddressBadSSLCom is the IP address of badssl.com.
const AddressBadSSLCom = "104.154.89.105"
Loading

0 comments on commit 18f9dcb

Please sign in to comment.