sqlx icon indicating copy to clipboard operation
sqlx copied to clipboard

Add client SSL authentication using key-file for Postgres, MySQL and MariaDB

Open ThibsG opened this issue 3 years ago • 7 comments

Revival of https://github.com/launchbadge/sqlx/pull/1166 Fixes #1162

Tests are separated whether client SSL or password is used to authenticate the user. Database's configuration is changing (to configure database and not provide any password, and thus be more confident in the test), but if you believe this is too heavy (too long, too many images etc.) let me know, I'll mix the Docker images to use the same, either we want to connect using SSL or with a password.

ThibsG avatar May 02 '22 13:05 ThibsG

Thanks for this, @ThibsG! We use YugabyteDB (it's PostgreSQL wire-compatible like CockroachDB) and need to use client certificates for authentication. We've been waiting for this support in order to shift from Diesel to SQLx.

Tagging @abonander since he reviewed the previous PR (#1166).

I'll do some testing on my end and report back. Thanks again!

RabidFire avatar May 31 '22 04:05 RabidFire

Hi @RabidFire , did you have some time to run some tests?

ThibsG avatar Jul 12 '22 07:07 ThibsG

I get Error: Configuration("no keys found pem file") when I use an elliptic curve key type (-----BEGIN EC PRIVATE KEY-----). But maybe this type of key is not supported by rustls? It is tested working with psql.

EnigmaCurry avatar Jul 14 '22 00:07 EnigmaCurry

I'd like to see the workflows running, not sure why they're not.

abonander avatar Jul 15 '22 20:07 abonander

@abonander it seems that if I push more than 1 time per day-ish, it won't run tests automatically (too heavy I suppose), so it needs approval from maintainer (the link I see is this one: https://docs.github.com/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks)

ThibsG avatar Jul 19 '22 16:07 ThibsG

PRs from first-time contributors require approval every time a workflow wants to be run. It's a bit annoying but it seems to be a decent precaution as an attacker could open a PR and modify a workflow to submit some secret environment variables like auth tokens to a server they control.

abonander avatar Jul 19 '22 21:07 abonander

Well thanks that makes perfect sense.

I just updated GA workflow to properly remove the previous container so the port is free to spawn the next container. Tests should run well now.

ThibsG avatar Jul 22 '22 09:07 ThibsG

I've been playing with this a bit, and one potential issue I have (for the use case I'm looking at) is that configuration of the client certs is dependent on accept_invalid_certs being false.

It seems to me that client certificate configuration should be independent of server certificate verification? WDYT?

fiadliel avatar Nov 27 '22 19:11 fiadliel

Indeed I totally agree, good catch thanks! :wink:

ThibsG avatar Nov 28 '22 19:11 ThibsG

@ThibsG it would also be nice to support combined pem files (cert + key in same file) as that would be rather convenient (and potentially reduce IO/reads in "heavy" envs).

something like.

async fn load_client_certs(client_cert: &CertificateInput) -> Result<(Vec<Certificate>, PrivateKey), Error> {
    let mut cursor = Cursor::new(client_cert.data().await?);

    let mut certs: Vec<Certificate> = vec! [];
    let mut keys: Vec<PrivateKey> = vec! [];

    loop {
        match rustls_pemfile::read_one(&mut cursor).expect("Could not parse cert/private keys from client cert") {
            Some(rustls_pemfile::Item::RSAKey(key)) => keys.push(PrivateKey(key)),
            Some(rustls_pemfile::Item::PKCS8Key(key)) => keys.push(PrivateKey(key)),
            Some(rustls_pemfile::Item::ECKey(key)) => keys.push(PrivateKey(key)),
            Some(rustls_pemfile::Item::X509Certificate(cert)) => certs.push(Certificate(cert)),
            None => break,
            _ => {}
        }
    }

    if keys.len() == 1 {
        Ok((certs, keys.into_iter().next().unwrap()))
    } else {
        Err(Error::Tls(format!("No private key in client cert").into()))
    }
}

srinivasmohan avatar Jan 12 '23 01:01 srinivasmohan

@ThibsG current development is on the 0.7-dev branch which just had some major refactors merged. Do you mind rebasing?

abonander avatar Feb 02 '23 00:02 abonander

@abonander done with rebasing 😉

ThibsG avatar Feb 04 '23 16:02 ThibsG

   Compiling sqlx-postgres v0.6.2 (https://github.com/ThibsG/sqlx?branch=postgres-native-tls-fixes#7ffc1330)
error[E0063]: missing fields `client_cert_path` and `client_key_path` in initializer of `TlsConfig<'_>`
  --> /home/ryan/.cargo/git/checkouts/sqlx-1c3a6f3a9c600558/7ffc133/sqlx-postgres/src/connection/tls.rs:56:18
   |
56 |     let config = TlsConfig {
   |                  ^^^^^^^^^ missing `client_cert_path` and `client_key_path`

EnigmaCurry avatar Feb 04 '23 19:02 EnigmaCurry

I'm testing this with setting PGSSLCERT, PGSSLKEY, and PGSSLROOTCERT environment vars. I'm getting this panic:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Tls(NotPkcs8)', src/main.rs:11:10

Heres my code and the full backtrace and how I set my env vars and my throwaway cert+key. I had suspected that my use of Elliptic Curve keys was causing this, so I switched my root CA, server, and client certs all to use RSA instead. AFAIK it did output a valid pkcs8 key file. It connects with psql correctly. I am generating the keys with step-ca in my script here.

In general, this is how I bring up postgres

EnigmaCurry avatar Feb 05 '23 22:02 EnigmaCurry

The EC key variant was not handled, this has been fixed, thanks!

Also, I changed the target branch to merge this PR into 0.7-dev, it will be easier to review changes.

ThibsG avatar Feb 06 '23 10:02 ThibsG

FWIW, I'm still getting the same error Tls(NotPkcs8) with either EC or RSA keys. There must be something strange about my certs (maybe to do with step-ca vs openssl?), as I don't get this error if I use instead the ones provided in the tests folder.

EnigmaCurry avatar Feb 06 '23 18:02 EnigmaCurry

I'm not a cert expert, but your client key seems to be in PKCS#1 format (the header is starting with BEGIN RSA PRIVATE KEY, not BEGIN PRIVATE KEY, in PKCS#8 the type is encoded within the key).

Looking at source code, it seems also that rust-native-tls does not support PKCS#1 keys (TL;DR: no need, the conversion to PKCS#8 is not that hard).

Try to convert using: openssl pkcs8 -topk8 -nocrypt -in cc-client-key -out cc-client-key.pk8. I will test this with your setup tomorrow.

ThibsG avatar Feb 06 '23 22:02 ThibsG

Try to convert using: openssl pkcs8 -topk8 -nocrypt -in cc-client-key -out cc-client-key.pk8. I will test this with your setup tomorrow.

That fixed it! :tada:

I can confirm this is working now. End-to-End mutual TLS ::) Thank you!

EnigmaCurry avatar Feb 06 '23 22:02 EnigmaCurry

@EnigmaCurry , you may also want to try runtime-tokio-rustls feature instead of runtime-tokio-native-tls, as Rustls supports PKCS#1 format. It avoids the conversion if you didn't want to use openssl at all.

ThibsG avatar Feb 07 '23 08:02 ThibsG

Rustls support PKCS#1 format.

Confirmed! I fixed my script so it outputs keyfiles in both formats, but now its nice to be able to use either one. Thanks again.

EnigmaCurry avatar Feb 07 '23 08:02 EnigmaCurry

Looks like it's green, glad to finally land this!

abonander avatar Feb 18 '23 00:02 abonander