Skip to content

Commit 91d7288

Browse files
woodruffwpyca-boringbot[bot]
andauthored
Cherry-pick #14542 (#14543)
* Further restrict DNS wildcards in name constraint matching (#14542) * Further restruct DNS wildcards in name constraint matching Signed-off-by: William Woodruff <william@yossarian.net> * Bump limbo Signed-off-by: William Woodruff <william@yossarian.net> --------- Signed-off-by: William Woodruff <william@yossarian.net> * CHANGELOG: record changes Signed-off-by: William Woodruff <william@yossarian.net> * Bump version to 46.0.6 Signed-off-by: William Woodruff <william@yossarian.net> * Bump x509-limbo and/or wycheproof in CI (#14127) * Bump x509-limbo and/or wycheproof in CI * Skip 9 cabf::cn testcases Signed-off-by: William Woodruff <william@yossarian.net> --------- Signed-off-by: William Woodruff <william@yossarian.net> Co-authored-by: pyca-boringbot[bot] <pyca-boringbot[bot]+106132319@users.noreply.github.com> * Add credit to CHANGELOG Signed-off-by: William Woodruff <william@yossarian.net> --------- Signed-off-by: William Woodruff <william@yossarian.net> Co-authored-by: pyca-boringbot[bot] <pyca-boringbot[bot]+106132319@users.noreply.github.com>
1 parent 06e120e commit 91d7288

File tree

9 files changed

+88
-55
lines changed

9 files changed

+88
-55
lines changed

.github/actions/fetch-vectors/action.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ runs:
99
with:
1010
repository: "C2SP/wycheproof"
1111
path: "wycheproof"
12-
# Latest commit on the wycheproof main branch, as of Sep 16, 2025.
13-
ref: "999222b8cce3c271eb1d1924f8c053a766ac8d2a" # wycheproof-ref
12+
# Latest commit on the wycheproof main branch, as of Jan 09, 2026.
13+
ref: "fca0d3ba9f1286c3af57801ace39c633e29a88f1" # wycheproof-ref
1414
persist-credentials: false
1515

1616
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
1717
with:
1818
repository: "C2SP/x509-limbo"
1919
path: "x509-limbo"
20-
# Latest commit on the x509-limbo main branch, as of Sep 16, 2025.
21-
ref: "28e6162b6f78ee5052b6a1011f812317d01db101" # x509-limbo-ref
20+
# Latest commit on the x509-limbo main branch, as of Mar 25, 2026.
21+
ref: "23a7ab348f27efef72d429f4faa92a3d8c23f720" # x509-limbo-ref
2222
persist-credentials: false

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ Changelog
33

44
.. _v46-0-5:
55

6+
46.0.6 - 2026-03-25
7+
~~~~~~~~~~~~~~~~~~~
8+
9+
* **SECURITY ISSUE**: Fixed a bug where name constraints were not applied
10+
to peer names during verification when the leaf certificate contains a
11+
wildcard DNS SAN. Ordinary X.509 topologies are not affected by this bug,
12+
including those used by the Web PKI. Credit to **Oleh Konko (1seal)** for
13+
reporting the issue. **CVE-2026-34073**
14+
615
46.0.5 - 2026-02-10
716
~~~~~~~~~~~~~~~~~~~
817

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ build-backend = "maturin"
1616

1717
[project]
1818
name = "cryptography"
19-
version = "46.0.5"
19+
version = "46.0.6"
2020
authors = [
2121
{ name = "The Python Cryptographic Authority and individual contributors", email = "cryptography-dev@python.org" },
2222
]
@@ -70,7 +70,7 @@ ssh = ["bcrypt >=3.1.5"]
7070
# All the following are used for our own testing.
7171
nox = ["nox[uv] >=2024.04.15"]
7272
test = [
73-
"cryptography_vectors==46.0.5",
73+
"cryptography_vectors==46.0.6",
7474
"pytest >=7.4.0",
7575
"pytest-benchmark >=4.0",
7676
"pytest-cov >=2.10.1",

src/cryptography/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"__version__",
1111
]
1212

13-
__version__ = "46.0.5"
13+
__version__ = "46.0.6"
1414

1515

1616
__author__ = "The Python Cryptographic Authority and individual contributors"

src/rust/cryptography-x509-verification/src/lib.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,7 @@ impl<'a, 'chain> NameChain<'a, 'chain> {
162162
// match exactly one subdomain of `foo.com`. Therefore, the NC's matching
163163
// set is a strict superset of any possible wildcard SAN pattern.
164164
match (DNSConstraint::new(constraint.0), DNSPattern::new(name.0)) {
165-
(Some(constraint), Some(name)) => {
166-
Ok(Applied(constraint.matches(name.inner_name())))
167-
}
165+
(Some(constraint), Some(name)) => Ok(Applied(constraint.matches(&name))),
168166
(_, None) => Err(ValidationError::new(ValidationErrorKind::Other(format!(
169167
"unsatisfiable DNS name constraint: malformed SAN {}",
170168
name.0

src/rust/cryptography-x509-verification/src/types.rs

Lines changed: 59 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -138,19 +138,6 @@ impl<'a> DNSPattern<'a> {
138138
},
139139
}
140140
}
141-
142-
/// Returns the inner `DNSName` within this `DNSPattern`, e.g.
143-
/// `foo.com` for `*.foo.com` or `example.com` for `example.com`.
144-
///
145-
/// This API must not be used to bypass pattern matching; it exists
146-
/// solely to enable checks that only require the inner name, such
147-
/// as Name Constraint checks.
148-
pub fn inner_name(&self) -> &DNSName<'a> {
149-
match self {
150-
DNSPattern::Exact(dnsname) => dnsname,
151-
DNSPattern::Wildcard(dnsname) => dnsname,
152-
}
153-
}
154141
}
155142

156143
/// A `DNSConstraint` represents a DNS name constraint as defined in [RFC 5280 4.2.1.10].
@@ -163,35 +150,45 @@ impl<'a> DNSConstraint<'a> {
163150
DNSName::new(pattern).map(Self)
164151
}
165152

166-
/// Returns true if this `DNSConstraint` matches the given name.
153+
/// Returns true if this `DNSConstraint` matches the given `DNSPattern`.
167154
///
168155
/// Constraint matching is defined by RFC 5280: any DNS name that can
169156
/// be constructed by simply adding zero or more labels to the left-hand
170157
/// side of the name satisfies the name constraint.
171158
///
172-
/// ```rust
173-
/// # use cryptography_x509_verification::types::{DNSConstraint, DNSName};
174-
/// let example_com = DNSName::new("example.com").unwrap();
175-
/// let badexample_com = DNSName::new("badexample.com").unwrap();
176-
/// let foo_example_com = DNSName::new("foo.example.com").unwrap();
177-
/// assert!(DNSConstraint::new(example_com.as_str()).unwrap().matches(&example_com));
178-
/// assert!(DNSConstraint::new(example_com.as_str()).unwrap().matches(&foo_example_com));
179-
/// assert!(!DNSConstraint::new(example_com.as_str()).unwrap().matches(&badexample_com));
180-
/// ```
181-
pub fn matches(&self, name: &DNSName<'_>) -> bool {
182-
// NOTE: This may seem like an obtuse way to perform label matching,
183-
// but it saves us a few allocations: doing a substring check instead
184-
// would require us to clone each string and do case normalization.
185-
// Note also that we check the length in advance: Rust's zip
186-
// implementation terminates with the shorter iterator, so we need
187-
// to first check that the candidate name is at least as long as
188-
// the constraint it's matching against.
189-
name.as_str().len() >= self.0.as_str().len()
190-
&& self
191-
.0
192-
.rlabels()
193-
.zip(name.rlabels())
194-
.all(|(a, o)| a.eq_ignore_ascii_case(o))
159+
/// On top of what RFC 5280 specifies, we define behavior for wildcard
160+
/// patterns (which are not covered by RFC 5280): a wildcard pattern
161+
/// matches a constraint if the pattern matches the constraint's inner name,
162+
/// _or_ if the pattern's inner name matches the constraint.
163+
/// This allows us to reject DNS names like `*.example.com` when
164+
/// the constraint is `example.com` or `bar.example.com`.
165+
pub fn matches(&self, name: &DNSPattern<'_>) -> bool {
166+
match name {
167+
DNSPattern::Exact(name) => {
168+
// NOTE: This may seem like an obtuse way to perform label matching,
169+
// but it saves us a few allocations: doing a substring check instead
170+
// would require us to clone each string and do case normalization.
171+
// Note also that we check the length in advance: Rust's zip
172+
// implementation terminates with the shorter iterator, so we need
173+
// to first check that the candidate name is at least as long as
174+
// the constraint it's matching against.
175+
name.as_str().len() >= self.0.as_str().len()
176+
&& self
177+
.0
178+
.rlabels()
179+
.zip(name.rlabels())
180+
.all(|(a, o)| a.eq_ignore_ascii_case(o))
181+
}
182+
DNSPattern::Wildcard(inner) => {
183+
// NOTE: This check is not as simple as a single pattern match,
184+
// since we need two subtly distinct cases here:
185+
// 1. Constraint `bar.example.com` on `*.example.com`
186+
// 2. Constraint `example.com` on `*.example.com`
187+
// The first cases is handled by `DNSPattern::matches`, and the second is handled
188+
// by `DNSConstraint::matches`.
189+
name.matches(&self.0) || self.matches(&DNSPattern::Exact(inner.clone()))
190+
}
191+
}
195192
}
196193
}
197194

@@ -597,14 +594,33 @@ mod tests {
597594
let example_com = DNSConstraint::new("example.com").unwrap();
598595

599596
// Exact domain and arbitrary subdomains match.
600-
assert!(example_com.matches(&DNSName::new("example.com").unwrap()));
601-
assert!(example_com.matches(&DNSName::new("foo.example.com").unwrap()));
602-
assert!(example_com.matches(&DNSName::new("foo.bar.baz.quux.example.com").unwrap()));
597+
assert!(example_com.matches(&DNSPattern::new("example.com").unwrap()));
598+
assert!(example_com.matches(&DNSPattern::new("foo.example.com").unwrap()));
599+
assert!(example_com.matches(&DNSPattern::new("foo.bar.baz.quux.example.com").unwrap()));
603600

604601
// Parent domains, distinct domains, and substring domains do not match.
605-
assert!(!example_com.matches(&DNSName::new("com").unwrap()));
606-
assert!(!example_com.matches(&DNSName::new("badexample.com").unwrap()));
607-
assert!(!example_com.matches(&DNSName::new("wrong.com").unwrap()));
602+
assert!(!example_com.matches(&DNSPattern::new("com").unwrap()));
603+
assert!(!example_com.matches(&DNSPattern::new("badexample.com").unwrap()));
604+
assert!(!example_com.matches(&DNSPattern::new("wrong.com").unwrap()));
605+
}
606+
607+
#[test]
608+
fn test_dnsconstraint_matches_wildcard() {
609+
let com = DNSConstraint::new("com").unwrap();
610+
let example_com = DNSConstraint::new("example.com").unwrap();
611+
let bar_example_com = DNSConstraint::new("bar.example.com").unwrap();
612+
let baz_bar_example_com = DNSConstraint::new("baz.bar.example.com").unwrap();
613+
let any_example_com = DNSPattern::new("*.example.com").unwrap();
614+
615+
assert!(com.matches(&any_example_com));
616+
assert!(example_com.matches(&any_example_com));
617+
assert!(bar_example_com.matches(&any_example_com));
618+
619+
// A constraint on `baz.bar.example.com` doesn't match `*.example.com`,
620+
// since `baz.bar.example.com` matches zero or more sublabels of
621+
// `baz.bar.example.com` while `*.example.com` matches exactly one
622+
// sublabel of `example.com`.
623+
assert!(!baz_bar_example_com.matches(&any_example_com));
608624
}
609625

610626
#[test]

tests/x509/verification/test_limbo.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@
8585
# with what webpki and rustls do, but inconsistent with Go and OpenSSL.
8686
"rfc5280::ca-as-leaf",
8787
"pathlen::validation-ignores-pathlen-in-leaf",
88+
# These CABF SAN/CN mismatch tests are pretty niche.
89+
"webpki::cn::ipv4-hex-mismatch",
90+
"webpki::cn::ipv4-leading-zeros-mismatch",
91+
"webpki::cn::ipv6-uppercase-mismatch",
92+
"webpki::cn::ipv6-uncompressed-mismatch",
93+
"webpki::cn::ipv6-non-rfc5952-mismatch",
94+
"webpki::cn::punycode-not-in-san",
95+
"webpki::cn::utf8-vs-punycode-mismatch",
96+
"webpki::cn::not-in-san",
97+
"webpki::cn::case-mismatch",
8898
}
8999

90100

vectors/cryptography_vectors/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
"__version__",
77
]
88

9-
__version__ = "46.0.5"
9+
__version__ = "46.0.6"

vectors/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "uv_build"
44

55
[project]
66
name = "cryptography_vectors"
7-
version = "46.0.5"
7+
version = "46.0.6"
88
authors = [
99
{name = "The Python Cryptographic Authority and individual contributors", email = "cryptography-dev@python.org"}
1010
]

0 commit comments

Comments
 (0)