commit c90b3974c1be075026e343d3685e5123ac880b30
Author: Zack Newman <zack@philomathiclife.com>
Date: Tue, 11 Feb 2025 20:18:38 -0700
init
Diffstat:
A | .gitignore | | | 2 | ++ |
A | Cargo.toml | | | 37 | +++++++++++++++++++++++++++++++++++++ |
A | LICENSE-APACHE | | | 177 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | LICENSE-MIT | | | 20 | ++++++++++++++++++++ |
A | README.md | | | 52 | ++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/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) [![crates-io]](https://crates.io/crates/postgres_rustls) [![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(|_| ()) })
+ }
+}