Skip to content
Merged
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
2 changes: 1 addition & 1 deletion pgdog/src/net/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ impl Stream {
}
}

/// Get the hostname identity (SAN dNSName, falling back to Subject CN)
/// Get the identity (SAN dNSName, then SAN URI, falling back to Subject CN)
/// from the client's TLS certificate, if any.
pub(crate) fn tls_identity(&self) -> Option<&str> {
self.tls_identity.as_deref()
Expand Down
62 changes: 53 additions & 9 deletions pgdog/src/net/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ pub(crate) fn tls_server_end_point(cert: &CertificateDer<'_>) -> Option<Vec<u8>>
Some(digest(algorithm, cert.as_ref()).as_ref().to_vec())
}

/// Extract the hostname identity from the peer's TLS certificate, if present.
/// Extract the identity from the peer's TLS certificate, if present.
pub(crate) fn peer_identity(conn: &ServerConnection) -> Option<String> {
identity_from_certs(conn.peer_certificates()?)
}
Expand All @@ -230,11 +230,16 @@ pub(crate) fn peer_certificate_present(conn: &ServerConnection) -> bool {
.is_some_and(|certs| !certs.is_empty())
}

/// Extract a hostname identity from the first certificate in the chain.
/// Extract an identity from the first certificate in the chain.
///
/// Prefers the first `dNSName` in the Subject Alternative Name extension and
/// falls back to the Subject CN. RFC 6125 deprecated CN for hostname identity,
/// and modern certificates often publish identity only via SAN.
/// Prefers the first `dNSName` in the Subject Alternative Name extension, then
/// the first `uniformResourceIdentifier`, and falls back to the Subject CN.
/// RFC 6125 deprecated CN for identity, and modern certificates often publish
/// it only via SAN.
///
/// The URI is matched verbatim, with no scheme-specific parsing: workload
/// identity platforms (SPIFFE, SPIRE, Istio, GKE) assert identity as a
/// `spiffe://` URI and issue no `dNSName`.
pub(crate) fn identity_from_certs(certs: &[CertificateDer<'_>]) -> Option<String> {
use x509_parser::certificate::X509Certificate;
use x509_parser::extensions::GeneralName;
Expand All @@ -243,12 +248,19 @@ pub(crate) fn identity_from_certs(certs: &[CertificateDer<'_>]) -> Option<String
let (_, cert) = X509Certificate::from_der(cert_der).ok()?;

if let Ok(Some(san)) = cert.subject_alternative_name() {
let dns_name = san.value.general_names.iter().find_map(|gn| match gn {
GeneralName::DNSName(name) => Some((*name).to_string()),
let names = &san.value.general_names;
let dns_name = names.iter().find_map(|gn| match gn {
GeneralName::DNSName(name) => Some(*name),
_ => None,
});
if dns_name.is_some() {
return dns_name;
let identity = dns_name.or_else(|| {
names.iter().find_map(|gn| match gn {
GeneralName::URI(uri) => Some(*uri),
_ => None,
})
});
if let Some(identity) = identity {
return Some(identity.to_string());
}
}

Expand Down Expand Up @@ -1393,6 +1405,38 @@ mod tests {
);
}

#[test]
fn san_uri_used_when_no_dns_name() {
let pem = include_str!("../../tests/tls/cert_san_uri_only.pem");
let certs: Vec<CertificateDer<'static>> =
rustls_pki_types::CertificateDer::pem_slice_iter(pem.as_bytes())
.collect::<Result<Vec<_>, _>>()
.expect("parse PEM");

// No dNSName and no CN: a workload identity certificate names itself
// with a URI only.
assert_eq!(
identity_from_certs(&certs).as_deref(),
Some("spiffe://example.svc.id.goog/ns/dashboard/sa/webapp")
);
}

#[test]
fn san_dns_preferred_over_san_uri() {
let pem = include_str!("../../tests/tls/cert_san_uri_and_dns.pem");
let certs: Vec<CertificateDer<'static>> =
rustls_pki_types::CertificateDer::pem_slice_iter(pem.as_bytes())
.collect::<Result<Vec<_>, _>>()
.expect("parse PEM");

// The URI comes first in the SAN, but dNSName still wins so
// certificates that resolve today keep resolving the same way.
assert_eq!(
identity_from_certs(&certs).as_deref(),
Some("primary.san.example")
);
}

#[test]
fn load_ca_bundle_loads_full_chain() {
crate::logger();
Expand Down
21 changes: 21 additions & 0 deletions pgdog/tests/tls/cert_san_uri_and_dns.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDdDCCAlygAwIBAgIUGTiyMpOVJoMNCDEvSNB3hKej88cwDQYJKoZIhvcNAQEL
BQAwHjEcMBoGA1UEAwwTZmFsbGJhY2stY24uZXhhbXBsZTAeFw0yNjEwMjUxMjM1
NDBaFw0zNjEwMjIxMjM1NDBaMB4xHDAaBgNVBAMME2ZhbGxiYWNrLWNuLmV4YW1w
bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6YvQJggyIT3siBBbo
FIBc7XusVH84r30TObHg/RK5pdFaSYiKJVNaBpGC94thaARZqI2acnNOV+pVJpd0
E/wg2rQ9I6VAA7jtcMDVgV3AaLmnVMmQp107PYLD9WqiWVv5Ofw1YBEoySCeBLsP
/MhLI4Erot4r3obl8m3I6gtqgvh2s2z1LCSPzhcp+RgHiuNY2jeNHJrdq8DEsz0v
1m0ybo8CyuTCnbOmZn48US7qb6aUIGvdHkC5W5gxxp08w1p+isF0FUOJY9eGkxyt
AurS3B9lce52htZp92gw5eS56YLgEXmRm/CQ6xNz5+FwJZLWRBidPQueVEWJcU9I
HqgzAgMBAAGjgakwgaYwHQYDVR0OBBYEFOKQBFGxP1/QHnDu6brWxhgBPxqjMB8G
A1UdIwQYMBaAFOKQBFGxP1/QHnDu6brWxhgBPxqjMA8GA1UdEwEB/wQFMAMBAf8w
UwYDVR0RBEwwSoYzc3BpZmZlOi8vZXhhbXBsZS5zdmMuaWQuZ29vZy9ucy9kYXNo
Ym9hcmQvc2Evd2ViYXBwghNwcmltYXJ5LnNhbi5leGFtcGxlMA0GCSqGSIb3DQEB
CwUAA4IBAQAMzMVOI+7DNCls8GXntv/9w04hIhxvjJpzSMIv2wIY+h/fnOXUoYfJ
NZQ/uziFPMuy4ju7e3B5L1fGt7iZDns7Rm2L6JcuWM1S1ifjANLoxEfuhLeY0r+e
HoX8mu6DwTyAFfpGuVGxrAN2JZX7WVY+2uO4h+d5Dxm2qB6U/rLK7RLsXf5vRA7O
KZa4pO+Lmfl02v2pOrLYx7XFevxGsZTP9WLPa4WTiy48/GdbiE1zM5Q5YGxhfGIk
2LoMSCgog14i0iFEorp1GqSpeD+BKGzeVSSFd4gK6oOQP/JPY6OEpK/Vlgv9PyEr
qDYEeV3TcejjclkBE8WCHvPMmPpDXQio
-----END CERTIFICATE-----
20 changes: 20 additions & 0 deletions pgdog/tests/tls/cert_san_uri_only.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDUzCCAjugAwIBAgIUWoex0yYBmTjA8ganm1fs2ReN6gQwDQYJKoZIhvcNAQEL
BQAwGDEWMBQGA1UECgwNU3BpZmZlRXhhbXBsZTAeFw0yNjEwMjUxMjM1NDlaFw0z
NjEwMjIxMjM1NDlaMBgxFjAUBgNVBAoMDVNwaWZmZUV4YW1wbGUwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC2WIbrDMJvRhGalt2MLpaMpmrn01y9c045
haZ+DLlGuTgVhlwKAnaMke2Vb3y/X4Dd7HHX0g9UCQPclsB7t5NKW8FNLqhy7v7m
FnrfICjF6dtw3a+qDpIMFn1f/vyu94GEXsI589MgePqzFGiYjmcxNWl3wofqbO5C
i/8BjnklbuKtqiM6GDx6Q953tcoP0/IuKHafSavrS1J84juBDjBusDFlmjIJVBbv
N8izVstu/vEhfJDyvbZoqBMENJrAEA4c2+oyoZAXzBdwOk2zxpmMAGiVFN9MeNIx
6Tqzp5emPWRzEu6tgfSm4Xf9LCxNMrggCkw8Yk2S/LsMeZ/DwbDHAgMBAAGjgZQw
gZEwHQYDVR0OBBYEFB9K8LtqVXZFqgTAoAJkZJqCZo7gMB8GA1UdIwQYMBaAFB9K
8LtqVXZFqgTAoAJkZJqCZo7gMA8GA1UdEwEB/wQFMAMBAf8wPgYDVR0RBDcwNYYz
c3BpZmZlOi8vZXhhbXBsZS5zdmMuaWQuZ29vZy9ucy9kYXNoYm9hcmQvc2Evd2Vi
YXBwMA0GCSqGSIb3DQEBCwUAA4IBAQBQz6fGNv1q2mes2ETI/5oITnDPZNT8xCJK
/w4fBlVq4lnlC0mkkY1a9/DAmXIBCX3IrObphpG2nytEHScAep4Erx78jRGm5eHC
MsowZXPyqZ9b429CV6QA6IRI6eX/e4vuj7uVuisISKVnSLcKEaSNs1mb++q5SMDw
fTZxJFIa16V+jmJwDR5T7bEMejNcpxszvBhWPjHk2f6QaVBINR7BVR7Hy8MNV25N
dcQYVPHG4HS3vVKxYCrfl2KLGJVPpBcMbOO6bNnZZ35MGByFTIn9/e+PgdWE4Vmh
aRsIhHG/eE6m3aWuA+7CQ7ysuxFcNEuGYOo7K912of0WJ6MEP+7b
-----END CERTIFICATE-----
Loading