diff --git a/pgdog/src/net/stream.rs b/pgdog/src/net/stream.rs index 796be2fd2..81b35605b 100644 --- a/pgdog/src/net/stream.rs +++ b/pgdog/src/net/stream.rs @@ -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() diff --git a/pgdog/src/net/tls.rs b/pgdog/src/net/tls.rs index c643cb1b1..ce1dc2910 100644 --- a/pgdog/src/net/tls.rs +++ b/pgdog/src/net/tls.rs @@ -218,7 +218,7 @@ pub(crate) fn tls_server_end_point(cert: &CertificateDer<'_>) -> Option> 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 { identity_from_certs(conn.peer_certificates()?) } @@ -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 { use x509_parser::certificate::X509Certificate; use x509_parser::extensions::GeneralName; @@ -243,12 +248,19 @@ pub(crate) fn identity_from_certs(certs: &[CertificateDer<'_>]) -> Option 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()); } } @@ -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> = + rustls_pki_types::CertificateDer::pem_slice_iter(pem.as_bytes()) + .collect::, _>>() + .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> = + rustls_pki_types::CertificateDer::pem_slice_iter(pem.as_bytes()) + .collect::, _>>() + .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(); diff --git a/pgdog/tests/tls/cert_san_uri_and_dns.pem b/pgdog/tests/tls/cert_san_uri_and_dns.pem new file mode 100644 index 000000000..86da6353b --- /dev/null +++ b/pgdog/tests/tls/cert_san_uri_and_dns.pem @@ -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----- diff --git a/pgdog/tests/tls/cert_san_uri_only.pem b/pgdog/tests/tls/cert_san_uri_only.pem new file mode 100644 index 000000000..e42adfe61 --- /dev/null +++ b/pgdog/tests/tls/cert_san_uri_only.pem @@ -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-----