postgres_rustls

Rustls-based library for postgres.
git clone https://git.philomathiclife.com/repos/postgres_rustls
Log | Files | Refs | README

commit c90b3974c1be075026e343d3685e5123ac880b30
Author: Zack Newman <zack@philomathiclife.com>
Date:   Tue, 11 Feb 2025 20:18:38 -0700

init

Diffstat:
A.gitignore | 2++
ACargo.toml | 37+++++++++++++++++++++++++++++++++++++
ALICENSE-APACHE | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE-MIT | 20++++++++++++++++++++
AREADME.md | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib.rs | 695+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 983 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/** diff --git a/Cargo.toml b/Cargo.toml @@ -0,0 +1,37 @@ +[package] +authors = ["Zack Newman <zack@philomathiclife.com>"] +categories = ["database"] +description = "TLS support for tokio-postgres via tokio-rustls." +documentation = "https://docs.rs/postgres_rustls/latest/postgres_rustls/" +edition = "2021" +keywords = ["postgres", "rustls", "tls", "tokio"] +license = "MIT OR Apache-2.0" +name = "postgres_rustls" +readme = "README.md" +repository = "https://git.philomathiclife.com/repos/postgres_rustls/" +rust-version = "1.84.0" +version = "0.1.0" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +rustls = { version = "0.23.22", default-features = false } +sha2 = { version = "0.10.8", default-features = false } +tokio = { version = "1.43.0", default-features = false } +tokio-postgres = { version = "0.7.13", default-features = false } +tokio-rustls = { version = "0.26.1", default-features = false } + +[dev-dependencies] +rustls = { version = "0.23.22", default-features = false, features = ["aws_lc_rs"] } +tokio = { version = "1.43.0", default-features = false, features = ["rt"] } + + +### FEATURES ################################################################# + +[features] +default = ["runtime"] + +# Provide MakeTlsConnect support. +runtime = ["tokio-postgres/runtime"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT @@ -0,0 +1,20 @@ +Copyright © 2025 Zack Newman + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +“Software”), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md @@ -0,0 +1,52 @@ +# `postgres_rustls` + +[<img alt="git" src="https://git.philomathiclife.com/badges/postgres_rustls.svg" height="20">](https://git.philomathiclife.com/postgres_rustls/log.html) +[<img alt="crates.io" src="https://img.shields.io/crates/v/postgres_rustls.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/postgres_rustls) +[<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-postgres_rustls-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs" height="20">](https://docs.rs/postgres_rustls/latest/postgres_rustls/) + +`postgres_rustls` is a library that adds TLS support to [`tokio-postgres`](https://crates.io/crates/tokio-postgres) +using [`tokio-rustls`](https://crates.io/crates/tokio-rustls). + +## Minimum Supported Rust Version (MSRV) + +This will frequently be updated to be the same as stable. Specifically, any time stable is updated and that +update has "useful" features or compilation no longer succeeds (e.g., due to new compiler lints), then MSRV +will be updated. + +MSRV changes will correspond to a SemVer patch version bump pre-`1.0.0`; otherwise a minor version bump. + +## SemVer Policy + +* All on-by-default features of this library are covered by SemVer +* MSRV is considered exempt from SemVer as noted above + +## License + +Licensed under either of + +* Apache License, Version 2.0 ([LICENSE-APACHE](https://www.apache.org/licenses/LICENSE-2.0)) +* MIT license ([LICENSE-MIT](https://opensource.org/licenses/MIT)) + +at your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, +as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +Before any PR is sent, `cargo clippy` and `cargo t` should be run _for each possible combination of "features"_ +using stable Rust. One easy way to achieve this is by building `ci` and invoking it with no commands in the +`postgres_rustls` directory or sub-directories. You can fetch `ci` via `git clone https://git.philomathiclife.com/repos/ci`, +and it can be built with `cargo build --release`. Additionally, +`RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to ensure documentation can be built. + +Note that when running `cargo t` you will need to make sure PostgreSQL is set up properly. You will likely need +to modify some of the test data like username to run the tests. + +### Status + +This package is actively maintained and will conform to the +[latest version of `tokio-postgres`](https://crates.io/crates/tokio-postgres). + +The crate is only tested on `x86_64-unknown-linux-gnu` and `x86_64-unknown-openbsd` targets, but it should work +on most platforms. diff --git a/src/lib.rs b/src/lib.rs @@ -0,0 +1,695 @@ +//! [![git]](https://git.philomathiclife.com/postgres_rustls/log.html)&ensp;[![crates-io]](https://crates.io/crates/postgres_rustls)&ensp;[![docs-rs]](crate) +//! +//! [git]: https://git.philomathiclife.com/git_badge.svg +//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust +//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs +//! +//! `postgres_rustls` is a library that adds TLS support to [`tokio_postgres`] based on [`tokio_rustls`]. +#![cfg_attr(docsrs, feature(doc_cfg))] +#![deny( + unknown_lints, + future_incompatible, + let_underscore, + missing_docs, + nonstandard_style, + refining_impl_trait, + rust_2018_compatibility, + rust_2018_idioms, + rust_2021_compatibility, + rust_2024_compatibility, + unsafe_code, + unused, + warnings, + clippy::all, + clippy::cargo, + clippy::complexity, + clippy::correctness, + clippy::nursery, + clippy::pedantic, + clippy::perf, + clippy::restriction, + clippy::style, + clippy::suspicious +)] +#![expect( + clippy::arbitrary_source_item_ordering, + clippy::blanket_clippy_restriction_lints, + clippy::implicit_return, + clippy::missing_trait_methods, + clippy::multiple_crate_versions, + clippy::single_call_fn, + reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs" +)] +use core::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; +use rustls::{ + pki_types::{InvalidDnsNameError, ServerName}, + ClientConfig, +}; +use sha2::{ + digest::{generic_array::GenericArray, Digest as _, OutputSizeUser}, + Sha224, Sha256, Sha384, Sha512, +}; +use std::io::{self, IoSlice}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +#[cfg(feature = "runtime")] +use tokio_postgres::tls::MakeTlsConnect; +use tokio_postgres::tls::{ChannelBinding, TlsConnect as PgTlsConnect, TlsStream as PgTlsStream}; +use tokio_rustls::{self, client::TlsStream as RustlsStream, Connect}; +/// Hash for the leaf certificate provided by the PostgreSQL server. +#[expect(clippy::doc_markdown, reason = "PostgreSQL is correct")] +enum Hash { + /// SHA-256 hash. + Sha256(GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize>), + /// SHA-384 hash. + Sha384(GenericArray<u8, <Sha384 as OutputSizeUser>::OutputSize>), + /// SHA-512 hash. + Sha512(GenericArray<u8, <Sha512 as OutputSizeUser>::OutputSize>), + /// SHA-224 hash. + Sha224(GenericArray<u8, <Sha224 as OutputSizeUser>::OutputSize>), +} +impl From<Hash> for Vec<u8> { + #[inline] + fn from(value: Hash) -> Self { + match value { + Hash::Sha256(hash) => hash.to_vec(), + Hash::Sha384(hash) => hash.to_vec(), + Hash::Sha512(hash) => hash.to_vec(), + Hash::Sha224(hash) => hash.to_vec(), + } + } +} +impl Hash { + /// Parses `cert` as a DER-encoded X.509 v3 certifcate extracting the signature algorithm and using it to hash + /// `cert` (based on the underlying hash algorithm of the signature algorithm). + /// + /// Note this will return `Some` for certain invalid payloads; however when the payload is a valid + /// DER-encoded X.509 v3 certificate, then it is guaranteed to produce the correct hash. The idea is that + /// in the event the leaf certificate is invalid, then it will be rejected by `rustls` anyway thus what + /// this returns won't matter. We do this for simplicity and performance reasons since it allows us to + /// avoid parsing the entire certificate and instead _only_ care about the signature algorithm. + /// + /// The only signature algorithms supported are the following: + /// + /// * id-Ed25519 + /// * ecdsa-with-SHA256 + /// * sha256WithRSAEncryption + /// * ecdsa-with-SHA384 + /// * sha384WithRSAEncryption + /// * ecdsa-with-SHA512 + /// * sha512WithRSAEncryption + /// * ecdsa-with-SHA224 + /// * sha224WithRSAEncryption + /// * dsa-with-SHA256 + /// * dsa-with-SHA224 + /// * dsa-with-SHA1 + /// * sha1WithRSAEncryption + /// + /// More may be added; but for ones that require additional dependencies, they will be hidden behind a feature. + // + // [RFC 5280 § 4.1](https://www.rfc-editor.org/rfc/rfc5280#section-4.1) describes the ASN.1 format + // of an X.509 v3 Certificate: + // + // ```asn + // Certificate ::= SEQUENCE { + // tbsCertificate TBSCertificate, + // signatureAlgorithm AlgorithmIdentifier, + // signatureValue BIT STRING } + // + // TBSCertificate ::= SEQUENCE { … } + // + // AlgorithmIdentifier ::= SEQUENCE { + // algorithm OBJECT IDENTIFIER, + // parameters ANY DEFINED BY algorithm OPTIONAL } + // ``` + // + // [ITU-T X.690](https://www.itu.int/rec/T-REC-X.690-202102-I/en) + // describes how DER-encoding works. + #[expect( + clippy::arithmetic_side_effects, + clippy::big_endian_bytes, + clippy::indexing_slicing, + reason = "comments justify their correctness" + )] + #[expect(clippy::too_many_lines, reason = "inflated by the const slices")] + fn from_der_cert(cert: &[u8]) -> Option<Self> { + /// [RFC 8410 § 9](https://www.rfc-editor.org/rfc/rfc8410#section-9) + /// defines id-Ed25519 as 1.3.101.112. This is encoded as below per X.690 § 8.19. + const ED25519: &[u8] = [43, 101, 112].as_slice(); + /// [RFC 5912 § 6](https://www.rfc-editor.org/rfc/rfc5912.html#section-6) + /// defines ecdsa-with-SHA256 as 1.2.840.10045.4.3.2. This is encoded as + /// below per X.690 § 8.19. + const ECDSA_SHA256: &[u8] = [42, 134, 72, 206, 61, 4, 3, 2].as_slice(); + /// [RFC 8017 § Appendix C](https://www.rfc-editor.org/rfc/rfc8017#appendix-C) + /// defines sha256WithRSAEncryption as 1.2.840.113549.1.1.11. This is encoded as + /// below per X.690 § 8.19. + const RSA_SHA256: &[u8] = [42, 134, 72, 134, 247, 13, 1, 1, 11].as_slice(); + /// [RFC 5912 § 6](https://www.rfc-editor.org/rfc/rfc5912.html#section-6) + /// defines dsa-with-SHA256 as 2.16.840.1.101.3.4.3.2. This is encoded as + /// below per X.690 § 8.19. + const DSA_SHA256: &[u8] = [96, 134, 72, 1, 101, 3, 4, 3, 2].as_slice(); + /// [RFC 5912 § 6](https://www.rfc-editor.org/rfc/rfc5912.html#section-6) + /// defines dsa-with-SHA1 as 1.2.840.10040.4.3. This is encoded as + /// below per X.690 § 8.19. + const DSA_SHA1: &[u8] = [42, 134, 72, 206, 56, 4, 3].as_slice(); + /// [RFC 8017 § Appendix C](https://www.rfc-editor.org/rfc/rfc8017#appendix-C) + /// defines sha1WithRSAEncryption as 1.2.840.113549.1.1.5. This is encoded as + /// below per X.690 § 8.19. + const RSA_SHA1: &[u8] = [42, 134, 72, 134, 247, 13, 1, 1, 5].as_slice(); + /// [RFC 5912 § 6](https://www.rfc-editor.org/rfc/rfc5912.html#section-6) + /// defines ecdsa-with-SHA384 as 1.2.840.10045.4.3.3. This is encoded as + /// below per X.690 § 8.19. + const ECDSA_SHA384: &[u8] = [42, 134, 72, 206, 61, 4, 3, 3].as_slice(); + /// [RFC 8017 § Appendix C](https://www.rfc-editor.org/rfc/rfc8017#appendix-C) + /// defines sha384WithRSAEncryption as 1.2.840.113549.1.1.12. This is encoded as + /// below per X.690 § 8.19. + const RSA_SHA384: &[u8] = [42, 134, 72, 134, 247, 13, 1, 1, 12].as_slice(); + /// [RFC 5912 § 6](https://www.rfc-editor.org/rfc/rfc5912.html#section-6) + /// defines ecdsa-with-SHA512 as 1.2.840.10045.4.3.4. This is encoded as + /// below per X.690 § 8.19. + const ECDSA_SHA512: &[u8] = [42, 134, 72, 206, 61, 4, 3, 4].as_slice(); + /// [RFC 8017 § Appendix C](https://www.rfc-editor.org/rfc/rfc8017#appendix-C) + /// defines sha512WithRSAEncryption as 1.2.840.113549.1.1.13. This is encoded as + /// below per X.690 § 8.19. + const RSA_SHA512: &[u8] = [42, 134, 72, 134, 247, 13, 1, 1, 13].as_slice(); + /// [RFC 5912 § 6](https://www.rfc-editor.org/rfc/rfc5912.html#section-6) + /// defines ecdsa-with-SHA224 as 1.2.840.10045.4.3.1. This is encoded as + /// below per X.690 § 8.19. + const ECDSA_SHA224: &[u8] = [42, 134, 72, 206, 61, 4, 3, 1].as_slice(); + /// [RFC 8017 § Appendix C](https://www.rfc-editor.org/rfc/rfc8017#appendix-C) + /// defines sha224WithRSAEncryption as 1.2.840.113549.1.1.14. This is encoded as + /// below per X.690 § 8.19. + const RSA_SHA224: &[u8] = [42, 134, 72, 134, 247, 13, 1, 1, 14].as_slice(); + /// [RFC 5912 § 6](https://www.rfc-editor.org/rfc/rfc5912.html#section-6) + /// defines dsa-with-SHA224 as 2.16.840.1.101.3.4.3.1. This is encoded as + /// below per X.690 § 8.19. + const DSA_SHA224: &[u8] = [96, 134, 72, 1, 101, 3, 4, 3, 1].as_slice(); + // The first octet represents a constructed sequence (i.e., 0x30) and the second octet represents + // the (possibly encoded) length of the remaining payload. + cert.split_at_checked(2).and_then(|(cert_seq, cert_rem)| { + // This won't `panic` since `cert_seq.len() == 2`. + // This represents the (possibly encoded) length. + // + // We don't care about the actual length of the payload. We only care about the number of bytes + // we need to skip until the (possibly encoded) length of the constructed sequence of + // tbsCertificate. + match cert_seq[1] { + // The length of the payload is represented with this byte; thus we only need to skip + // the constructed sequence byte (i.e., 0x30) of tbsCertificate. + ..=127 => Some(1), + // The length of the payload is invalid since DER-encoded lengths must use the minimum + // number of bytes possible. The high bit is set iff one or more octets are needed to encode + // the actual length. The number of octets is represented by the remaining bits; thus this + // means there are 0 bytes to encode the length, but then it should have been encoded as + // `0` and not `128`. + // + // 255 is not allowed to be used per § 8.1.3.5. + 128 | 255 => None, + // The remaining bits represent the number of bytes that it takes to encode the length; + // thus we subtract 128 and add 1 to account for the constructed sequence byte (i.e., 0x30) + // of tbsCertificate. This is the same as just subtracting 127. + // + // Underflow clearly cannot occur since `len` is at least 129. + len @ 129.. => Some(usize::from(len) - 127), + } + .and_then(|skip| { + cert_rem.get(skip..).and_then(|tbs_rem_with_len| { + // Remaining payload starting from the (possibly encoded) length of tbsCertificate. + tbs_rem_with_len + // Extract the (possibly encoded) length of tbsCertificate. + .split_first() + .and_then(|(tbs_enc_len, tbs_rem)| { + // We need to extract how many bytes make up tbsCertificate that way + // we can skip it and get to the (possibly encoded) length of + // signatureAlgorithm. + match *tbs_enc_len { + // tbsCertificate is encoded in `len` bytes. We need to skip that many + // bytes taking into account the constructed sequence byte (i.e., 0x30) + // of signatureAlgorithm. + // + // This won't overflow since `len` is at most 127; thus this maxes at 128 + // which is <= `usize::MAX`. + len @ ..=127 => Some(usize::from(len) + 1), + // The number of bytes tbsCertificate is encoded in takes 1 byte to encode + // We get that one byte since that is how many bytes we need to skip taking + // into account the byte and the constructed sequence byte (i.e., 0x30) of + // signatureAlgorithm. + // + // This won't overflow since this maxes at 255 + 2 = 257 <= usize::MAX. + 129 => tbs_rem.first().map(|len| usize::from(*len) + 2), + // The number of bytes tbsCertificate is encoded in takes 2 bytes to encode. + // We get the two bytes since that is how many bytes we need to skip. + 130 => tbs_rem.get(..2).and_then(|enc_len| { + let mut big_endian_len = [0; 2]; + // This won't `panic` since `enc_len.len() == 2`. + big_endian_len.copy_from_slice(enc_len); + // Multi-byte lengths are encoded in big-endian. + u16::from_be_bytes(big_endian_len) + // We need to account for the two bytes and the constructed sequence byte + // (i.e., 0x30) of signatureAlgorithm. + .checked_add(3) + // We don't support payloads larger than 65,535 since that is more than + // enough for a single certificate. + .map(usize::from) + }), + // We arbitrarily cap the size of payloads we accept to simplify decoding. + // If this is more than 130, then the payload is at least 16,777,216 bytes + // which is obscenely large for a single certificate. 128 is invalid. + _ => None, + } + .and_then(|tbs_len| { + tbs_rem.get(tbs_len..).and_then(|alg_rem_with_len| { + // Remaining payload starting from the (possibly encoded) length of + // signatureAlgorithm. + alg_rem_with_len + // Extract the (possibly encoded) length of signatureAlgorithm. + .split_first() + .and_then(|(alg_enc_len, alg_rem)| { + // We need to extract how many bytes make up signatureAlgorithm that way + // we can skip it and get to the (possibly encoded) length of algorithm. + match *alg_enc_len { + // The length of the payload is represented with this byte; thus we + // only need to skip the object identifier byte (i.e., 0x06) of + // algorithm. + ..=127 => Some(1), + // The length of the payload is invalid. + 128 | 255 => None, + // The remaining bits represents the number of bytes that it takes to + // encode the length; thus we subtract 128 and add 1 to account for + // the object identifier byte (i.e., 0x06) of algorithm. + // This is the same as just subtracting 127. + // + // Underflow clearly cannot occur since `len` is at least 129. + len @ 129.. => Some(usize::from(len) - 127), + } + .and_then( + |alg_skip| { + alg_rem.get(alg_skip..).and_then(|oid_rem| { + // Remaining payload starting from the (possibly encoded) + // length of algorithm, and we extract the + // (possibly encoded) length of algorithm. + oid_rem.split_first().and_then( + |(oid_enc_len, rem)| { + // Extract the algorithm. + // Recall we don't care if the certificate is + // invalid, and we only support algorithms of + // length at most 127. As a result, we treat + // `oid_enc_len` as is. + rem.get(..usize::from(*oid_enc_len)) + .and_then(|oid| match oid { + ED25519 | ECDSA_SHA256 + | RSA_SHA256 | DSA_SHA256 + // [RFC 5929 § 4.1](https://www.rfc-editor.org/rfc/rfc5929#section-4.1) + // mandates that SHA-1 based signatures + // use SHA-256. + | DSA_SHA1 | RSA_SHA1 => { + Some(Self::Sha256( + Sha256::digest( + cert, + ), + )) + } + ECDSA_SHA384 + | RSA_SHA384 => { + Some(Self::Sha384( + Sha384::digest( + cert, + ), + )) + } + ECDSA_SHA512 + | RSA_SHA512 => { + Some(Self::Sha512( + Sha512::digest( + cert, + ), + )) + } + ECDSA_SHA224 + | RSA_SHA224 + | DSA_SHA224 => { + Some(Self::Sha224( + Sha224::digest( + cert, + ), + )) + } + _ => None, + }) + }, + ) + }) + }, + ) + }) + }) + }) + }) + }) + }) + }) + } +} +/// The [`TlsConnector::Stream`] returned from [`TlsConnectorFuture::poll`]. +pub struct TlsStream<S>(RustlsStream<S>); +impl<S: AsyncRead + AsyncWrite + Unpin> AsyncRead for TlsStream<S> { + #[inline] + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll<io::Result<()>> { + Pin::new(&mut self.0).poll_read(cx, buf) + } +} +impl<S: AsyncRead + AsyncWrite + Unpin> AsyncWrite for TlsStream<S> { + #[inline] + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll<Result<usize, io::Error>> { + Pin::new(&mut self.0).poll_write(cx, buf) + } + #[inline] + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> { + Pin::new(&mut self.0).poll_flush(cx) + } + #[inline] + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll<Result<(), io::Error>> { + Pin::new(&mut self.0).poll_shutdown(cx) + } + #[inline] + fn poll_write_vectored( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[IoSlice<'_>], + ) -> Poll<Result<usize, io::Error>> { + Pin::new(&mut self.0).poll_write_vectored(cx, bufs) + } + #[inline] + fn is_write_vectored(&self) -> bool { + self.0.is_write_vectored() + } +} +impl<S: AsyncRead + AsyncWrite + Unpin> PgTlsStream for TlsStream<S> { + /// Returns the [`ChannelBinding`] based on the X.509 v3 certificate sent from the PostgreSQL server. + /// + /// Note when this returns [`ChannelBinding::tls_server_end_point`], it _does not_ mean the certificate + /// is valid. In certain circumstances, this will return that even for an invalid certificate. This should + /// not matter since the certificate being invalid will cause the certificate to be rejected anyway. When + /// the certificate is valid and uses a supported signature algorithm, then this will always return the + /// correct value. + /// + /// The only supported signature algorithms are the following: + /// + /// * id-Ed25519 + /// * ecdsa-with-SHA256 + /// * sha256WithRSAEncryption + /// * ecdsa-with-SHA384 + /// * sha384WithRSAEncryption + /// * ecdsa-with-SHA512 + /// * sha512WithRSAEncryption + /// * ecdsa-with-SHA224 + /// * sha224WithRSAEncryption + /// * dsa-with-SHA256 + /// * dsa-with-SHA224 + /// * dsa-with-SHA1 + /// * sha1WithRSAEncryption + /// + /// Note it is strongly recommended that TLS 1.3 be used; thus while signature algorithms that are not + /// part of TLS 1.3 are supported, you should avoid them. + /// See [RFC 9266 § 4.2](https://www.rfc-editor.org/rfc/rfc9266#section-4.2). + #[expect(clippy::doc_markdown, reason = "PostgreSQL is correct")] + #[inline] + fn channel_binding(&self) -> ChannelBinding { + self.0 + .get_ref() + .1 + .peer_certificates() + .and_then(|certs| { + certs.first().and_then(|fst| { + Hash::from_der_cert(fst) + .map(|hash| ChannelBinding::tls_server_end_point(hash.into())) + }) + }) + .unwrap_or_else(ChannelBinding::none) + } +} +/// [`TlsConnector::Future`] returned from [`TlsConnector::connect`]. +pub struct TlsConnectorFuture<S>(Connect<S>); +impl<S: AsyncRead + AsyncWrite + Unpin> Future for TlsConnectorFuture<S> { + type Output = io::Result<TlsStream<S>>; + #[inline] + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + match Pin::new(&mut self.0).poll(cx) { + Poll::Ready(res) => Poll::Ready(res.map(TlsStream)), + Poll::Pending => Poll::Pending, + } + } +} +/// Connects to the PostgreSQL server. +#[expect(clippy::doc_markdown, reason = "PostgreSQL is correct")] +pub struct TlsConnector<'domain> { + /// Used to connect to the PostgreSQL server. + #[expect(clippy::doc_markdown, reason = "PostgreSQL is correct")] + connector: tokio_rustls::TlsConnector, + /// The domain or IP of the PostgreSQL server. + #[expect(clippy::doc_markdown, reason = "PostgreSQL is correct")] + dom: ServerName<'domain>, +} +impl<'domain> TlsConnector<'domain> { + /// Returns `Self` based on `connector` and `domain`. + /// + /// # Errors + /// + /// Errors iff [`ServerName::try_from`] does when passed `domain`. + #[inline] + pub fn new<'dom: 'domain>( + connector: tokio_rustls::TlsConnector, + domain: &'dom str, + ) -> Result<Self, InvalidDnsNameError> { + ServerName::try_from(domain).map(|dom| Self { connector, dom }) + } +} +impl<S: AsyncRead + AsyncWrite + Unpin> PgTlsConnect<S> for TlsConnector<'static> { + type Stream = TlsStream<S>; + type Error = io::Error; + type Future = TlsConnectorFuture<S>; + #[inline] + fn connect(self, stream: S) -> Self::Future { + TlsConnectorFuture(self.connector.connect(self.dom, stream)) + } +} +/// [`MakeTlsConnect`] based on [`tokio_rustls::TlsConnector`]. +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +#[cfg(feature = "runtime")] +#[derive(Clone)] +pub struct MakeTlsConnector(tokio_rustls::TlsConnector); +#[cfg(feature = "runtime")] +impl MakeTlsConnector { + /// Constructs `Self` based on `connector`. + #[inline] + #[must_use] + pub const fn new(connector: tokio_rustls::TlsConnector) -> Self { + Self(connector) + } +} +#[cfg(feature = "runtime")] +impl<S: AsyncRead + AsyncWrite + Unpin> MakeTlsConnect<S> for MakeTlsConnector { + type Stream = TlsStream<S>; + type TlsConnect = TlsConnector<'static>; + type Error = InvalidDnsNameError; + #[inline] + fn make_tls_connect(&mut self, domain: &str) -> Result<Self::TlsConnect, Self::Error> { + ServerName::try_from(domain).map(|dom| TlsConnector { + connector: self.0.clone(), + dom: dom.to_owned(), + }) + } +} +/// Removes any ALPN values and adds the `b"postgresql"` ALPN. +#[inline] +pub fn set_postgresql_alpn(config: &mut ClientConfig) { + config.alpn_protocols.clear(); + config.alpn_protocols.push(vec![ + b'p', b'o', b's', b't', b'g', b'r', b'e', b's', b'q', b'l', + ]); +} +#[cfg(test)] +mod tests { + #[cfg(feature = "runtime")] + extern crate alloc; + use super::Hash; + #[cfg(feature = "runtime")] + use super::MakeTlsConnector; + #[cfg(feature = "runtime")] + use alloc::sync::Arc; + #[cfg(feature = "runtime")] + use core::{ + net::{IpAddr, Ipv6Addr}, + time::Duration, + }; + #[cfg(feature = "runtime")] + use rustls::{pki_types::CertificateDer, version::TLS13, ClientConfig, RootCertStore}; + #[cfg(feature = "runtime")] + use std::io::Error; + #[cfg(feature = "runtime")] + use tokio::runtime::Builder; + #[cfg(feature = "runtime")] + use tokio_postgres::{ + config::{ChannelBinding, LoadBalanceHosts, SslMode, TargetSessionAttrs}, + Config, + }; + /// [ISRG Root X1](https://letsencrypt.org/certs/isrgrootx1.der). + const ISRG_ROOT_X1: &[u8; 1391] = &[ + 48, 130, 5, 107, 48, 130, 3, 83, 160, 3, 2, 1, 2, 2, 17, 0, 130, 16, 207, 176, 210, 64, + 227, 89, 68, 99, 224, 187, 99, 130, 139, 0, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, + 11, 5, 0, 48, 79, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 83, 49, 41, 48, 39, 6, 3, 85, + 4, 10, 19, 32, 73, 110, 116, 101, 114, 110, 101, 116, 32, 83, 101, 99, 117, 114, 105, 116, + 121, 32, 82, 101, 115, 101, 97, 114, 99, 104, 32, 71, 114, 111, 117, 112, 49, 21, 48, 19, + 6, 3, 85, 4, 3, 19, 12, 73, 83, 82, 71, 32, 82, 111, 111, 116, 32, 88, 49, 48, 30, 23, 13, + 49, 53, 48, 54, 48, 52, 49, 49, 48, 52, 51, 56, 90, 23, 13, 51, 53, 48, 54, 48, 52, 49, 49, + 48, 52, 51, 56, 90, 48, 79, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 83, 49, 41, 48, 39, + 6, 3, 85, 4, 10, 19, 32, 73, 110, 116, 101, 114, 110, 101, 116, 32, 83, 101, 99, 117, 114, + 105, 116, 121, 32, 82, 101, 115, 101, 97, 114, 99, 104, 32, 71, 114, 111, 117, 112, 49, 21, + 48, 19, 6, 3, 85, 4, 3, 19, 12, 73, 83, 82, 71, 32, 82, 111, 111, 116, 32, 88, 49, 48, 130, + 2, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 2, 15, 0, 48, 130, + 2, 10, 2, 130, 2, 1, 0, 173, 232, 36, 115, 244, 20, 55, 243, 155, 158, 43, 87, 40, 28, 135, + 190, 220, 183, 223, 56, 144, 140, 110, 60, 230, 87, 160, 120, 247, 117, 194, 162, 254, 245, + 106, 110, 246, 0, 79, 40, 219, 222, 104, 134, 108, 68, 147, 182, 177, 99, 253, 20, 18, 107, + 191, 31, 210, 234, 49, 155, 33, 126, 209, 51, 60, 186, 72, 245, 221, 121, 223, 179, 184, + 255, 18, 241, 33, 154, 75, 193, 138, 134, 113, 105, 74, 102, 102, 108, 143, 126, 60, 112, + 191, 173, 41, 34, 6, 243, 228, 192, 230, 128, 174, 226, 75, 143, 183, 153, 126, 148, 3, + 159, 211, 71, 151, 124, 153, 72, 35, 83, 232, 56, 174, 79, 10, 111, 131, 46, 209, 73, 87, + 140, 128, 116, 182, 218, 47, 208, 56, 141, 123, 3, 112, 33, 27, 117, 242, 48, 60, 250, 143, + 174, 221, 218, 99, 171, 235, 22, 79, 194, 142, 17, 75, 126, 207, 11, 232, 255, 181, 119, + 46, 244, 178, 123, 74, 224, 76, 18, 37, 12, 112, 141, 3, 41, 160, 225, 83, 36, 236, 19, + 217, 238, 25, 191, 16, 179, 74, 140, 63, 137, 163, 97, 81, 222, 172, 135, 7, 148, 244, 99, + 113, 236, 46, 226, 111, 91, 152, 129, 225, 137, 92, 52, 121, 108, 118, 239, 59, 144, 98, + 121, 230, 219, 164, 154, 47, 38, 197, 208, 16, 225, 14, 222, 217, 16, 142, 22, 251, 183, + 247, 168, 247, 199, 229, 2, 7, 152, 143, 54, 8, 149, 231, 226, 55, 150, 13, 54, 117, 158, + 251, 14, 114, 177, 29, 155, 188, 3, 249, 73, 5, 216, 129, 221, 5, 180, 42, 214, 65, 233, + 172, 1, 118, 149, 10, 15, 216, 223, 213, 189, 18, 31, 53, 47, 40, 23, 108, 210, 152, 193, + 168, 9, 100, 119, 110, 71, 55, 186, 206, 172, 89, 94, 104, 157, 127, 114, 214, 137, 197, 6, + 65, 41, 62, 89, 62, 221, 38, 245, 36, 201, 17, 167, 90, 163, 76, 64, 31, 70, 161, 153, 181, + 167, 58, 81, 110, 134, 59, 158, 125, 114, 167, 18, 5, 120, 89, 237, 62, 81, 120, 21, 11, 3, + 143, 141, 208, 47, 5, 178, 62, 123, 74, 28, 75, 115, 5, 18, 252, 198, 234, 224, 80, 19, + 124, 67, 147, 116, 179, 202, 116, 231, 142, 31, 1, 8, 208, 48, 212, 91, 113, 54, 180, 7, + 186, 193, 48, 48, 92, 72, 183, 130, 59, 152, 166, 125, 96, 138, 162, 163, 41, 130, 204, + 186, 189, 131, 4, 27, 162, 131, 3, 65, 161, 214, 5, 241, 27, 194, 182, 240, 168, 124, 134, + 59, 70, 168, 72, 42, 136, 220, 118, 154, 118, 191, 31, 106, 165, 61, 25, 143, 235, 56, 243, + 100, 222, 200, 43, 13, 10, 40, 255, 247, 219, 226, 21, 66, 212, 34, 208, 39, 93, 225, 121, + 254, 24, 231, 112, 136, 173, 78, 230, 217, 139, 58, 198, 221, 39, 81, 110, 255, 188, 100, + 245, 51, 67, 79, 2, 3, 1, 0, 1, 163, 66, 48, 64, 48, 14, 6, 3, 85, 29, 15, 1, 1, 255, 4, 4, + 3, 2, 1, 6, 48, 15, 6, 3, 85, 29, 19, 1, 1, 255, 4, 5, 48, 3, 1, 1, 255, 48, 29, 6, 3, 85, + 29, 14, 4, 22, 4, 20, 121, 180, 89, 230, 123, 182, 229, 228, 1, 115, 128, 8, 136, 200, 26, + 88, 246, 233, 155, 110, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 3, 130, 2, + 1, 0, 85, 31, 88, 169, 188, 178, 168, 80, 208, 12, 177, 216, 26, 105, 32, 39, 41, 8, 172, + 97, 117, 92, 138, 110, 248, 130, 229, 105, 47, 213, 246, 86, 75, 185, 184, 115, 16, 89, + 211, 33, 151, 126, 231, 76, 113, 251, 178, 210, 96, 173, 57, 168, 11, 234, 23, 33, 86, 133, + 241, 80, 14, 89, 235, 206, 224, 89, 233, 186, 201, 21, 239, 134, 157, 143, 132, 128, 246, + 228, 233, 145, 144, 220, 23, 155, 98, 27, 69, 240, 102, 149, 210, 124, 111, 194, 234, 59, + 239, 31, 207, 203, 214, 174, 39, 241, 169, 176, 200, 174, 253, 125, 126, 154, 250, 34, 4, + 235, 255, 217, 127, 234, 145, 43, 34, 177, 23, 14, 143, 242, 138, 52, 91, 88, 216, 252, 1, + 201, 84, 185, 184, 38, 204, 138, 136, 51, 137, 76, 45, 132, 60, 130, 223, 238, 150, 87, 5, + 186, 44, 187, 247, 196, 183, 199, 78, 59, 130, 190, 49, 200, 34, 115, 115, 146, 209, 194, + 128, 164, 57, 57, 16, 51, 35, 130, 76, 60, 159, 134, 178, 85, 152, 29, 190, 41, 134, 140, + 34, 155, 158, 226, 107, 59, 87, 58, 130, 112, 77, 220, 9, 199, 137, 203, 10, 7, 77, 108, + 232, 93, 142, 201, 239, 206, 171, 199, 187, 181, 43, 78, 69, 214, 74, 208, 38, 204, 229, + 114, 202, 8, 106, 165, 149, 227, 21, 161, 247, 164, 237, 201, 44, 95, 165, 251, 255, 172, + 40, 2, 46, 190, 215, 123, 187, 227, 113, 123, 144, 22, 211, 7, 94, 70, 83, 124, 55, 7, 66, + 140, 211, 196, 150, 156, 213, 153, 181, 42, 224, 149, 26, 128, 72, 174, 76, 57, 7, 206, + 204, 71, 164, 82, 149, 43, 186, 184, 251, 173, 210, 51, 83, 125, 229, 29, 77, 109, 213, + 161, 177, 199, 66, 111, 230, 64, 39, 53, 92, 163, 40, 183, 7, 141, 231, 141, 51, 144, 231, + 35, 159, 251, 80, 156, 121, 108, 70, 213, 180, 21, 179, 150, 110, 126, 155, 12, 150, 58, + 184, 82, 45, 63, 214, 91, 225, 251, 8, 194, 132, 254, 36, 168, 163, 137, 218, 172, 106, + 225, 24, 42, 177, 168, 67, 97, 91, 211, 31, 220, 59, 141, 118, 242, 45, 232, 141, 117, 223, + 23, 51, 108, 61, 83, 251, 123, 203, 65, 95, 255, 220, 162, 208, 97, 56, 225, 150, 184, 172, + 93, 139, 55, 215, 117, 213, 51, 192, 153, 17, 174, 157, 65, 193, 114, 117, 132, 190, 2, 65, + 66, 95, 103, 36, 72, 148, 209, 155, 39, 190, 7, 63, 185, 184, 79, 129, 116, 81, 225, 122, + 183, 237, 157, 35, 226, 190, 224, 213, 40, 4, 19, 60, 49, 3, 158, 221, 122, 108, 143, 198, + 7, 24, 198, 127, 222, 71, 142, 63, 40, 158, 4, 6, 207, 165, 84, 52, 119, 189, 236, 137, + 155, 233, 23, 67, 223, 91, 219, 95, 254, 142, 30, 87, 162, 205, 64, 157, 126, 98, 34, 218, + 222, 24, 39, + ]; + #[test] + fn test_parse() { + assert!( + Hash::from_der_cert(ISRG_ROOT_X1).map_or(false, |hash| matches!(hash, Hash::Sha256(_))), + "postgres_rustls::Hash::from_der_cert could not extract the signature algorithm from ISRG Root X1" + ); + } + #[cfg(feature = "runtime")] + #[derive(Debug)] + enum E { + #[expect(dead_code, reason = "does not matter for tests")] + Io(Error), + #[expect(dead_code, reason = "does not matter for tests")] + Rustls(rustls::Error), + #[expect(dead_code, reason = "does not matter for tests")] + Postgres(tokio_postgres::Error), + } + #[cfg(feature = "runtime")] + impl From<Error> for E { + fn from(value: Error) -> Self { + Self::Io(value) + } + } + #[cfg(feature = "runtime")] + impl From<rustls::Error> for E { + fn from(value: rustls::Error) -> Self { + Self::Rustls(value) + } + } + #[cfg(feature = "runtime")] + impl From<tokio_postgres::Error> for E { + fn from(value: tokio_postgres::Error) -> Self { + Self::Postgres(value) + } + } + #[cfg(feature = "runtime")] + #[test] + fn test_local_tls_connection() -> Result<(), E> { + /// Name of the database in PostgreSQL that we connect to. + const DBNAME: &str = "dbname"; + /// Name of the domain the certificate is issued to that PostgreSQL uses. + const HOST: &str = "host"; + /// The password needed to connect to [`DBNAME`]. + const PASSWORD: &[u8] = b"password".as_slice(); + /// Name of the user that will be used to connect to [`DBNAME`]. + const USER: &str = "user"; + let mut root_store = RootCertStore::empty(); + root_store.add(CertificateDer::from_slice(ISRG_ROOT_X1))?; + let mut conf = ClientConfig::builder_with_protocol_versions([&TLS13].as_slice()) + .with_root_certificates(root_store) + .with_no_client_auth(); + super::set_postgresql_alpn(&mut conf); + let mut config = Config::new(); + let connection = config + .application_name("test") + .channel_binding(ChannelBinding::Require) + .connect_timeout(Duration::from_secs(4)) + .dbname(DBNAME) + .host(HOST) + .hostaddr(IpAddr::V6(Ipv6Addr::LOCALHOST)) + .keepalives(false) + .load_balance_hosts(LoadBalanceHosts::Disable) + .password(PASSWORD) + .port(5432) + .ssl_mode(SslMode::Require) + .target_session_attrs(TargetSessionAttrs::Any) + .user(USER) + .connect(MakeTlsConnector::new(Arc::new(conf).into())); + Builder::new_current_thread() + .enable_all() + .build()? + .block_on(async move { connection.await.map_err(E::Postgres).map(|_| ()) }) + } +}