main.rs 6.76 KB
Newer Older
1
use anyhow::{anyhow, bail};
Alexandru Scvortov's avatar
Alexandru Scvortov committed
2
3
4
5

type OrError<T> = Result<T, anyhow::Error>;

fn main() -> OrError<()> {
Alexandru Scvortov's avatar
Alexandru Scvortov committed
6
    match std::env::args().nth(1).as_deref() {
Alexandru Scvortov's avatar
Alexandru Scvortov committed
7
8
9
        None => bail!("Pass at least one argument"),
        Some("experiment1") => experiment1()?,
        Some("experiment2") => experiment2()?,
10
        Some("experiment3") => experiment3()?,
Alexandru Scvortov's avatar
Alexandru Scvortov committed
11
12
13
14
15
        Some(str) => bail!("Unknown experiment: {}", str),
    }
    Ok(())
}

16
17
18
19
20
21
22
23
24
/// Execute a GET 1.1.1.1 request over HTTPS.  This blows up with the
/// following error:
///
/// ```
/// Error: https://1.1.1.1/: Dns Failed: InvalidDNSNameError
///
/// Caused by:
///     InvalidDNSNameError
/// ```
Alexandru Scvortov's avatar
Alexandru Scvortov committed
25
fn experiment1() -> OrError<()> {
26
27
    let resp: String = ureq::get("https://1.1.1.1").call()?.into_string()?;
    println!("Response:\n{}", resp);
Alexandru Scvortov's avatar
Alexandru Scvortov committed
28
29
30
    Ok(())
}

31
32
/// Try to make a `webpki::DnsNameRef` out of `1.1.1.1`.  This fails
/// with the expected error.
Alexandru Scvortov's avatar
Alexandru Scvortov committed
33
fn experiment2() -> OrError<()> {
34
35
36
    use webpki::DNSNameRef;
    let _: DNSNameRef = DNSNameRef::try_from_ascii_str("1.1.1.1")
        .map_err(|err| anyhow!("Failed to make a DNSNameRef: {:?}", err))?;
Alexandru Scvortov's avatar
Alexandru Scvortov committed
37
38
    Ok(())
}
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

/// Per https://github.com/algesten/ureq/issues/393#issuecomment-859676345
/// and https://scvalex.net/posts/48/, implement a workaround.
fn experiment3() -> OrError<()> {
    use certificate_verifier_no_hostname::CertificateVerifierNoHostname;
    use fixed_resolver::FixedResolver;
    use rustls::ClientConfig;
    use std::sync::Arc;
    use url::Url;

    // `original_url` is our original URL, `ip_addr` is the extracted
    // IP address, and `url` is the updated URL with the dummy
    // hostname.
    let original_url: Url = "https://1.1.1.1".parse()?;
    let ip_addr = original_url.host().unwrap().to_string();
Alexandru Scvortov's avatar
Alexandru Scvortov committed
54
    #[allow(clippy::redundant_clone)]
55
56
57
58
59
    let mut url = original_url.clone();
    url.set_host(Some("dummy-hostname-for-workaround"))?;

    // Create a TLS config with the default certificate roots and a
    // certificate verifier that ignores hostnames.
Alexandru Scvortov's avatar
Alexandru Scvortov committed
60
61
62
63
    //
    // TODO Configure just your own CA here to get any security from
    // the certificate verification.  Otherwise, any valid certificate
    // will be accepted.
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
    let mut tls_config = ClientConfig::new();
    tls_config
        .root_store
        .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
    tls_config
        .dangerous()
        .set_certificate_verifier(Arc::new(CertificateVerifierNoHostname));

    // Execute the `ureq` call with the special TLS config and a
    // resolver that always returns `ip_addr`.
    //
    // The extra tricks here are that we have to specify a port so
    // that we can parse a `SocketAddr` (although it's not used for
    // anything), and we have to set the `Host` header in case the
    // remote host is dispatching on that.
    let resp: String = ureq::builder()
        .tls_config(Arc::new(tls_config))
        .resolver(FixedResolver {
            ip_addr: format!("{}:443", ip_addr).parse()?,
        })
        .build()
        .request_url("GET", &url)
        .set("Host", &ip_addr)
        .call()?
        .into_string()?;
    println!("Response:\n{}", resp);
    Ok(())
}

mod fixed_resolver {
    //! A DNS resolver that always returns the same value regardless of
    //! the host it was queried for.

    use std::net::SocketAddr;

    pub struct FixedResolver {
        pub ip_addr: SocketAddr,
    }

    impl ureq::Resolver for FixedResolver {
        fn resolve(&self, _netloc: &str) -> std::io::Result<Vec<SocketAddr>> {
            Ok(vec![self.ip_addr])
        }
    }
}

mod certificate_verifier_no_hostname {
    use rustls::{
        Certificate, OwnedTrustAnchor, RootCertStore, ServerCertVerified, ServerCertVerifier,
        TLSError,
    };
    use std::time::SystemTime;
    use webpki::{DNSNameRef, EndEntityCert, SignatureAlgorithm, TrustAnchor};

    pub struct CertificateVerifierNoHostname;

    static SUPPORTED_SIG_ALGS: &[&SignatureAlgorithm] = &[
        &webpki::ECDSA_P256_SHA256,
        &webpki::ECDSA_P256_SHA384,
        &webpki::ECDSA_P384_SHA256,
        &webpki::ECDSA_P384_SHA384,
        &webpki::ED25519,
        &webpki::RSA_PSS_2048_8192_SHA256_LEGACY_KEY,
        &webpki::RSA_PSS_2048_8192_SHA384_LEGACY_KEY,
        &webpki::RSA_PSS_2048_8192_SHA512_LEGACY_KEY,
        &webpki::RSA_PKCS1_2048_8192_SHA256,
        &webpki::RSA_PKCS1_2048_8192_SHA384,
        &webpki::RSA_PKCS1_2048_8192_SHA512,
        &webpki::RSA_PKCS1_3072_8192_SHA384,
    ];

    impl ServerCertVerifier for CertificateVerifierNoHostname {
        /// Will verify the certificate is valid in the following ways:
        /// - Signed by a valid root
        /// - Not Expired
        ///
        /// Based on a https://github.com/ctz/rustls/issues/578#issuecomment-816712636
        fn verify_server_cert(
            &self,
            roots: &RootCertStore,
            intermediates: &[Certificate],
            _dns_name: DNSNameRef<'_>,
            ocsp_response: &[u8],
        ) -> Result<ServerCertVerified, TLSError> {
            // Get the end-entity cert, tthe chain, and the trust root. Error out if
            // chain is empty.
            let (cert, chain, trustroots) = prepare(roots, intermediates)?;

            // Validate the certificate is valid, signed by a trusted root, and not
            // expired.
            let now = SystemTime::now();
            let webpki_now =
                webpki::Time::try_from(now).map_err(|_| TLSError::FailedToGetCurrentTime)?;

            let _cert: EndEntityCert = cert
                .verify_is_valid_tls_server_cert(
                    SUPPORTED_SIG_ALGS,
                    &webpki::TLSServerTrustAnchors(&trustroots),
                    &chain,
                    webpki_now,
                )
                .map_err(TLSError::WebPKIError)
                .map(|_| cert)?;

            if !ocsp_response.is_empty() {
                //trace!("Unvalidated OCSP response: {:?}", ocsp_response.to_vec());
            }
            Ok(ServerCertVerified::assertion())
        }
    }

    #[allow(clippy::type_complexity)]
    fn prepare<'a, 'b>(
        roots: &'b RootCertStore,
        presented_certs: &'a [Certificate],
    ) -> Result<(EndEntityCert<'a>, Vec<&'a [u8]>, Vec<TrustAnchor<'b>>), TLSError> {
        if presented_certs.is_empty() {
            return Err(TLSError::NoCertificatesPresented);
        }

        // EE cert must appear first.
        let cert =
            webpki::EndEntityCert::from(&presented_certs[0].0).map_err(TLSError::WebPKIError)?;

        let chain: Vec<&'a [u8]> = presented_certs
            .iter()
            .skip(1)
            .map(|cert| cert.0.as_ref())
            .collect();

        let trustroots: Vec<webpki::TrustAnchor> = roots
            .roots
            .iter()
            .map(OwnedTrustAnchor::to_trust_anchor)
            .collect();

        Ok((cert, chain, trustroots))
    }
}