Skip to content

feat(tonic-xds): gRFC A29 cert validation building blocks#2616

Merged
YutaoMa merged 6 commits into
grpc:masterfrom
YutaoMa:yutaoma/xds-a29-cert-validation
May 12, 2026
Merged

feat(tonic-xds): gRFC A29 cert validation building blocks#2616
YutaoMa merged 6 commits into
grpc:masterfrom
YutaoMa:yutaoma/xds-a29-cert-validation

Conversation

@YutaoMa
Copy link
Copy Markdown
Contributor

@YutaoMa YutaoMa commented Apr 29, 2026

Motivation

Ref: #2444

Continues the gRFC A29 (xDS-Based TLS Security) work in tonic-xds. The cert provider foundation merged in #2593 gives us a pluggable source of trust roots and identity. The next step toward end-to-end mTLS in the data plane is to parse the cluster's TLS config from xDS, validate server certs against the configured trust roots, and apply SAN matching on top of WebPKI chain validation.

This PR adds the building blocks for that validation. Integration with the
connector factory is deferred to a follow-up PR — see below.

Solution

  • StringMatcher extraction (xds/resource/string_matcher.rs): pulled out of routing.rs so it can be reused. The SAN matcher uses it for the string-comparison primitives.
  • UpstreamTlsContext parsing (xds/resource/security.rs): parses envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext into a typed config.
  • SAN matcher (xds/resource/san_matcher.rs): RFC 6125 wildcard DNS matching, RFC 5952 IP canonicalization, plus URI, EMAIL, and OTHER_NAME paths matching using the above extracted StringMatcher.
  • A29 ServerCertVerifier (xds/cert_provider/verifier.rs): wraps WebPkiServerVerifier and applies SAN matching after standard chain validation. Sources trust roots from the existing CertProviderRegistry.

All new types are tested in isolation against synthetic certs.

Why integration is deferred

The tonic-side hook for installing a custom rustls ServerCertVerifier on a Channel is still in flight as #2612. Without that API, the verifier can't be wired into the per-cluster connector from a downstream crate.

UpstreamTlsContext parsing, SAN matching, and an A29 ServerCertVerifier
impl, plus a StringMatcher extraction reused by SAN matching. Connector
wiring deferred to a follow-up PR.
@YutaoMa YutaoMa marked this pull request as ready for review April 29, 2026 22:12
@YutaoMa YutaoMa requested review from ankurmittal and gu0keno0 April 29, 2026 22:12
Comment thread tonic-xds/src/xds/resource/security.rs
GeneralName::URI(s) => Some(SanEntry::Uri(s.to_string())),
GeneralName::RFC822Name(s) => Some(SanEntry::Email(s.to_string())),
GeneralName::IPAddress(bytes) => parse_ip_san(bytes).map(SanEntry::IpAddress),
GeneralName::OtherName(oid, value) => Some(SanEntry::OtherName {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be something like:

                                                                                                                                               
  GeneralName::OtherName(oid, raw) => {
      // raw = `[0] EXPLICIT ANY` DER. Pull out the inner Any, then take its
      // content bytes — typically a UTF8String / IA5String / PrintableString.                                                                 
      let (_, outer) = der_parser::ber::parse_ber(raw).ok()?;                                                                                  
      let inner = outer.as_slice().ok()?;          // contents of [0] EXPLICIT                                                                 
      let (_, any) = der_parser::ber::parse_ber(inner).ok()?;                                                                                  
      let value = any.as_slice().ok()?.to_vec();   // inner string bytes                                                                       
      Some(SanEntry::OtherName { oid: oid.to_id_string(), value })                                                                             
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah if we want to properly parse OtherName in SAN this will be needed. To avoid this complexity, since A29 only defined support for the above 4 SAN types (DNS, URI, Email, IP), I've removed all logic related to OtherName and rejects it at config time instead.

Comment thread tonic-xds/src/xds/resource/security.rs
ocsp_response: &[u8],
now: UnixTime,
) -> Result<ServerCertVerified, RustlsError> {
self.inner.verify_server_cert(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this support SPIFFE-only cert ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out, actually the hostname-checking nature of the WebPkiServerVerifier would cause SPIFFE cert to be rejected always. I refactored the verifier to only use rustls's chain verification impl and now all SAN checking is handled by our xDS matcher logic, allowing URI-only SPIFFE certs use case.

Comment thread tonic-xds/src/xds/resource/security.rs Outdated
Comment thread tonic-xds/src/xds/resource/san_matcher.rs Outdated
Comment thread tonic-xds/src/xds/resource/string_matcher.rs Outdated
Comment thread tonic-xds/src/xds/cert_provider/verifier.rs Outdated
oid: oid.to_id_string(),
value: value.to_vec(),
}),
_ => None,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we pass it along to SanEntry as a "Unspecified" variant? I'm not familiar with the other variants, yet I feel maybe there is a chance that the custom verifier may need to validate against them?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the above comment, no SAN matching behavior of other variants is defined in A29 and other gRPC impls (grpc-go, grpc-java) or skip those types. So I'll keep the None case here and also dropping the OtherName variant as well. Updated comments to reflect this.

Comment on lines +149 to +155
fn parse_ip_san(bytes: &[u8]) -> Option<IpAddr> {
match bytes.len() {
4 => <[u8; 4]>::try_from(bytes).ok().map(IpAddr::from),
16 => <[u8; 16]>::try_from(bytes).ok().map(IpAddr::from),
_ => None,
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://github.com/rusticata/x509-parser/blob/b7dcc9397b596cf9fa3df65115c3f405f1748b2a/src/extensions/mod.rs#L824 , ip addr can also be 8 bytes or 32 bytes because of network masks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

according to RFC 5280 4.2.1.6, IP SAN is 4/16 bytes. The 8/32 bytes is for CA nameConstraints which happens to be represented with the same extensions type here in x509-parser. I've updated the comments here to reflect this.

Copy link
Copy Markdown
Collaborator

@gu0keno0 gu0keno0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall, put a few comments for details.

YutaoMa added 2 commits May 11, 2026 16:51
support SPIFFE certs, drop OtherName support, better UTF8 string
handling and better comments
@YutaoMa YutaoMa merged commit 49d28e6 into grpc:master May 12, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants