Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ jobs:
run: cargo clippy --features pg-type-postgis -- -D warnings
- name: Lint minimal
run: cargo clippy --no-default-features -- -D warnings
- name: Lint server-api without tls
- name: Lint server-api (rustcrypto, no tls provider)
run: cargo clippy --no-default-features --features server-api -- -D warnings
- name: Lint ring
run: cargo clippy --no-default-features --features server-api-ring -- -D warnings
- name: Lint client api
run: cargo clippy --features client-api-aws-lc-rs -- -D warnings
run: cargo clippy --no-default-features --features client-api-aws-lc-rs -- -D warnings

test:
name: Test
Expand All @@ -58,13 +58,13 @@ jobs:
run: cargo test --features pg-type-postgis
- name: Run tests on minimal feature set
run: cargo test --no-default-features
- name: Run tests without tls
- name: Run tests without tls provider
run: cargo test --no-default-features --features server-api
- name: Run tests on additional ring feature set
run: cargo test --no-default-features --features server-api-ring
- name: Run tests for client api
if: runner.os != 'Windows'
run: cargo test --features client-api-aws-lc-rs
run: cargo test --no-default-features --features client-api-aws-lc-rs,pg-ext-types
- name: Run check sqlite example
run: cargo check --all-targets --features _sqlite,_bundled

Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- SCRAM-SHA-256 authentication and `tls-server-end-point` channel binding are
now implemented with pure-Rust RustCrypto crates (`sha2`, `hmac`, `pbkdf2`,
`x509-cert`) instead of `ring`/`aws-lc-rs`, and are always available with
the `server-api` and `client-api` features. `ring` and `aws-lc-rs` are no
longer direct dependencies of pgwire; they only appear (via `tokio-rustls`)
when a `*-ring`/`*-aws-lc-rs` feature is selected. The plain `server-api`
and `client-api` features now include the TLS types backed by a provider-less
`rustls`: the application selects the rustls crypto provider, following the
rustls "bring your own provider" recommendation. Bare `client-api` (without
a provider suffix) now compiles standalone. The `simple-oidc-validator`
feature uses the `rust_crypto` backend of `jsonwebtoken` instead of
`aws-lc-rs`.

### Added

- Client API: transaction status tracking. `ClientInfo::transaction_status`
Expand Down
75 changes: 46 additions & 29 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,22 @@ pin-project = { version = "1.1", optional = true }
rand = { version = "0.10", optional = true }
md5 = { version = "0.8", optional = true }
hex = { version = "0.4", optional = true }
## scram libraries
## scram libraries (RustCrypto)
base64 = { version = "0.23", optional = true }
ring = { version = "0.17", optional = true }
aws-lc-rs = { version = "1.7", optional = true }
stringprep = { version = "0.1.2", optional = true }
x509-certificate = { version = "0.25", optional = true }
sha2 = { version = "0.10", optional = true }
hmac = { version = "0.12", optional = true }
pbkdf2 = { version = "0.12", optional = true }
x509-cert = { version = "0.3", default-features = false, features = [
"pem",
"std",
], optional = true }

## oauth
jsonwebtoken = { version = "11", features = ["aws_lc_rs"], optional = true }
jsonwebtoken = { version = "11", default-features = false, features = [
"use_pem",
"rust_crypto",
], optional = true }
rsa = { version = "0.9", optional = true }
reqwest = { version = "0.13", features = ["json", "rustls-no-provider"], optional = true }

Expand All @@ -65,21 +72,17 @@ percent-encoding = { version = "2.0", optional = true }
[features]
default = ["server-api-aws-lc-rs", "pg-ext-types"]
pg-ext-types = ["pg-type-chrono", "pg-type-rust-decimal", "pg-type-serde-json"]
_ring = [
"dep:ring",
"tokio-rustls/ring",
"dep:rustls-pki-types",
"dep:base64",
"dep:stringprep",
"dep:x509-certificate",
]
_aws-lc-rs = [
"dep:aws-lc-rs",
"tokio-rustls/aws-lc-rs",

## Shared: TLS types (provider-less rustls) and SCRAM (pure-Rust RustCrypto)
_tls-scram = [
"dep:tokio-rustls",
"dep:rustls-pki-types",
"dep:base64",
"dep:stringprep",
"dep:x509-certificate",
"dep:sha2",
"dep:hmac",
"dep:pbkdf2",
"dep:x509-cert",
]
server-api = [
"dep:tokio",
Expand All @@ -92,16 +95,8 @@ server-api = [
"dep:postgres-types",
"dep:serde_json",
"dep:pg_interval",
"_tls-scram",
]
simple-oidc-validator = [
"dep:jsonwebtoken",
"dep:rsa",
"dep:reqwest",
"dep:base64",
"dep:serde",
]
server-api-ring = ["server-api", "_ring"]
server-api-aws-lc-rs = ["server-api", "_aws-lc-rs"]
client-api = [
"dep:percent-encoding",
"dep:pin-project",
Expand All @@ -110,13 +105,32 @@ client-api = [
"dep:futures",
"dep:async-trait",
"dep:md5",
"dep:hex",
"dep:rand",
"dep:postgres-types",
"dep:pg_interval",
"dep:serde_json",
"_tls-scram",
]
client-api-ring = ["client-api", "_ring", "dep:rustls-pki-types"]
client-api-aws-lc-rs = ["client-api", "_aws-lc-rs", "dep:rustls-pki-types"]

## Compatibility aliases: additionally select a default rustls crypto provider
## for the application. With the plain `server-api`/`client-api` features, the
## application chooses its own provider (rustls "bring your own provider").
server-api-ring = ["server-api", "tokio-rustls/ring"]
server-api-aws-lc-rs = ["server-api", "tokio-rustls/aws-lc-rs"]
client-api-ring = ["client-api", "tokio-rustls/ring"]
client-api-aws-lc-rs = ["client-api", "tokio-rustls/aws-lc-rs"]
pg-type-chrono = ["dep:chrono", "postgres-types/with-chrono-0_4"]
pg-type-rust-decimal = ["dep:rust_decimal"]
pg-type-serde-json = ["dep:serde", "dep:serde_json", "postgres-types/with-serde_json-1"]
pg-type-postgis = ["dep:postgis"]
simple-oidc-validator = [
"dep:jsonwebtoken",
"dep:rsa",
"dep:reqwest",
"dep:base64",
"dep:serde",
]
_sqlite = []
_bundled = ["rusqlite/bundled"]

Expand All @@ -140,7 +154,10 @@ gluesql = { version = "0.20", default-features = false, features = [
] }

## oauth
jsonwebtoken = { version = "11", features = ["aws_lc_rs"] }
jsonwebtoken = { version = "11", default-features = false, features = [
"use_pem",
"rust_crypto",
] }
rsa = { version = "0.9" }
reqwest = { version = "0.13", features = ["json"] }

Expand Down
4 changes: 0 additions & 4 deletions src/api/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ pub struct DefaultServerParameterProvider {
pub search_path: String,
pub is_superuser: bool,
pub default_transaction_read_only: bool,
#[cfg(any(feature = "_aws-lc-rs", feature = "_ring"))]
pub scram_iterations: usize,
// format settings
pub time_zone: String,
Expand All @@ -98,7 +97,6 @@ impl Default for DefaultServerParameterProvider {
search_path: "public".to_owned(),
is_superuser: true,
default_transaction_read_only: false,
#[cfg(any(feature = "_aws-lc-rs", feature = "_ring"))]
scram_iterations: sasl::scram::SCRAM_ITERATIONS,

time_zone: format_options.time_zone,
Expand Down Expand Up @@ -143,7 +141,6 @@ impl ServerParameterProvider for DefaultServerParameterProvider {
"default_transaction_read_only".to_owned(),
bool_to_string(self.default_transaction_read_only),
);
#[cfg(any(feature = "_aws-lc-rs", feature = "_ring"))]
params.insert(
"scram_iterations".to_owned(),
self.scram_iterations.to_string(),
Expand Down Expand Up @@ -375,7 +372,6 @@ where
pub mod cleartext;
pub mod md5pass;
pub mod noop;
#[cfg(any(feature = "_aws-lc-rs", feature = "_ring"))]
pub mod sasl;
#[cfg(feature = "simple-oidc-validator")]
pub mod simple_oidc_validator;
93 changes: 62 additions & 31 deletions src/api/auth/sasl/scram.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
use std::borrow::Cow;
use std::fmt;
use std::fmt::Write;
use std::num::NonZeroU32;
use std::ops::BitXor;
use std::str::{FromStr, Split};
use std::sync::Arc;

use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use x509_certificate::SignatureAlgorithm;
use x509_certificate::certificate::CapturedX509Certificate;
use hmac::{Hmac, Mac};
use pbkdf2::pbkdf2_hmac;
use sha2::{Digest, Sha256, Sha384, Sha512};
use x509_cert::Certificate;
use x509_cert::der::Decode;
use x509_cert::der::oid::ObjectIdentifier;

use crate::api::ClientInfo;
/// Re-exports client-side SCRAM authentication types.
Expand All @@ -21,11 +24,6 @@ use crate::messages::startup::{Authentication, PasswordMessageFamily};

use super::SASLState;

#[cfg(feature = "_aws-lc-rs")]
use aws_lc_rs::{digest, hmac, pbkdf2};
#[cfg(all(feature = "_ring", not(feature = "_aws-lc-rs")))]
use ring::{digest, hmac, pbkdf2};

/// Default SCRAM iteration count.
pub const SCRAM_ITERATIONS: usize = 4096;

Expand Down Expand Up @@ -779,23 +777,24 @@ impl<'a> ScamMessageChunker<'a> {
fn hi(normalized_password: &[u8], salt: &[u8], iterations: usize) -> Vec<u8> {
let mut buf = [0u8; 32];

pbkdf2::derive(
pbkdf2::PBKDF2_HMAC_SHA256,
NonZeroU32::new(iterations as u32).unwrap(),
salt,
pbkdf2_hmac::<Sha256>(
normalized_password,
salt,
iterations.try_into().expect("iterations out of u32 range"),
&mut buf,
);
buf.to_vec()
}

fn hmac(key: &[u8], msg: &[u8]) -> Vec<u8> {
let mac = hmac::Key::new(hmac::HMAC_SHA256, key);
hmac::sign(&mac, msg).as_ref().to_vec()
let mut mac =
<Hmac<Sha256> as Mac>::new_from_slice(key).expect("HMAC can take key of any size");
mac.update(msg);
mac.finalize().into_bytes().to_vec()
}

fn h(msg: &[u8]) -> Vec<u8> {
digest::digest(&digest::SHA256, msg).as_ref().to_vec()
Sha256::digest(msg).to_vec()
}

fn xor(lhs: &[u8], rhs: &[u8]) -> Vec<u8> {
Expand All @@ -805,6 +804,16 @@ fn xor(lhs: &[u8], rhs: &[u8]) -> Vec<u8> {
.collect()
}

/// Signature algorithm OIDs relevant to `tls-server-end-point` channel
/// binding, as defined in RFC 5929 section 4.1
/// (<https://www.rfc-editor.org/rfc/rfc5929#section-4.1>).
const OID_RSA_SHA1: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.5");
const OID_RSA_SHA256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
const OID_RSA_SHA384: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.12");
const OID_RSA_SHA512: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.13");
const OID_ECDSA_SHA256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
const OID_ECDSA_SHA384: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.3");

/// Compute signature of server certificate for `tls-server-end-point` channel
/// binding.
///
Expand All @@ -815,23 +824,24 @@ fn xor(lhs: &[u8], rhs: &[u8]) -> Vec<u8> {
/// 2. use the certificate's algorithm if it's neither md5 or sha-1
/// 3. if the certificate has 0 or more than 1 signature algorithm, the
/// behaviour is undefined at the time.
fn compute_cert_signature(cert: &[u8]) -> PgWireResult<Vec<u8>> {
let certs = CapturedX509Certificate::from_pem_multiple(cert)
fn compute_cert_signature(cert_pem: &[u8]) -> PgWireResult<Vec<u8>> {
// Hash the DER encoding of the first certificate in the pem data.
let (label, der_bytes) = x509_cert::der::pem::decode_vec(cert_pem)
.map_err(|e| PgWireError::ApiError(Box::new(e)))?;
let x509 = &certs[0];
let raw = x509.constructed_data();
match x509.signature_algorithm() {
Some(SignatureAlgorithm::RsaSha1)
| Some(SignatureAlgorithm::RsaSha256)
| Some(SignatureAlgorithm::EcdsaSha256) => {
Ok(digest::digest(&digest::SHA256, raw).as_ref().to_vec())
}
Some(SignatureAlgorithm::RsaSha384) | Some(SignatureAlgorithm::EcdsaSha384) => {
Ok(digest::digest(&digest::SHA384, raw).as_ref().to_vec())
}
Some(SignatureAlgorithm::RsaSha512) => {
Ok(digest::digest(&digest::SHA512, raw).as_ref().to_vec())
}
if label != "CERTIFICATE" {
return Err(PgWireError::ApiError(Box::new(std::io::Error::other(
format!("unexpected PEM label: {label}"),
))));
}

let certificate =
Certificate::from_der(&der_bytes).map_err(|e| PgWireError::ApiError(Box::new(e)))?;

match certificate.signature_algorithm().oid {
// md5 and sha-1 based signatures are re-hashed with sha-256
OID_RSA_SHA1 | OID_RSA_SHA256 | OID_ECDSA_SHA256 => Ok(Sha256::digest(&der_bytes).to_vec()),
OID_RSA_SHA384 | OID_ECDSA_SHA384 => Ok(Sha384::digest(&der_bytes).to_vec()),
OID_RSA_SHA512 => Ok(Sha512::digest(&der_bytes).to_vec()),
_ => Err(PgWireError::UnsupportedCertificateSignatureAlgorithm),
}
}
Expand Down Expand Up @@ -927,6 +937,27 @@ mod tests {
);
}

#[test]
fn test_compute_cert_signature_with_rsa_sha256_cert() {
// examples/ssl/server.crt is signed with sha256WithRSAEncryption, so
// the tls-server-end-point hash must be SHA-256 over its DER data.
let pem = std::fs::read("examples/ssl/server.crt").unwrap();
let signature = compute_cert_signature(&pem).unwrap();

let (label, der) = x509_cert::der::pem::decode_vec(&pem).unwrap();
assert_eq!(label, "CERTIFICATE");
assert_eq!(signature, Sha256::digest(der).to_vec());
assert_eq!(signature.len(), 32);
}

#[test]
fn test_compute_cert_signature_rejects_non_certificate_pem() {
let err = compute_cert_signature(
b"-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n",
);
assert!(err.is_err());
}

#[cfg(feature = "client-api")]
#[test]
fn test_auth_roundtrip() -> PgWireClientResult<()> {
Expand Down
Loading
Loading