Skip to content

Commit bdb7874

Browse files
Gealdjc
authored andcommitted
add a method to collect the DNS names from a cert.
This commit adds an `EndEntityCert::dns_names` method, which returns a list of the DNS names provided in the subject alternative names extension of the certificate. Authored-by: Geoffroy Couprie [email protected] Co-authored-by: Sean McArthur [email protected] Co-authored-by: Eliza Weisman [email protected] Co-authored-by: Daniel McCarney [email protected] Signed-off-by: Daniel McCarney [email protected]
1 parent 6dd4a44 commit bdb7874

File tree

8 files changed

+286
-34
lines changed

8 files changed

+286
-34
lines changed

src/end_entity.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1313
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1414

15+
#[cfg(feature = "alloc")]
16+
use crate::subject_name::GeneralDnsNameRef;
1517
use crate::{
1618
cert, signed_data, subject_name, verify_cert, Error, SignatureAlgorithm, SubjectNameRef, Time,
1719
TlsClientTrustAnchors, TlsServerTrustAnchors,
@@ -174,4 +176,17 @@ impl<'a> EndEntityCert<'a> {
174176
untrusted::Input::from(signature),
175177
)
176178
}
179+
180+
/// Returns a list of the DNS names provided in the subject alternative names extension
181+
///
182+
/// This function must not be used to implement custom DNS name verification.
183+
/// Verification functions are already provided as `verify_is_valid_for_dns_name`
184+
/// and `verify_is_valid_for_at_least_one_dns_name`.
185+
///
186+
/// Requires the `alloc` default feature; i.e. this isn't available in
187+
/// `#![no_std]` configurations.
188+
#[cfg(feature = "alloc")]
189+
pub fn dns_names(&'a self) -> Result<impl Iterator<Item = GeneralDnsNameRef<'a>>, Error> {
190+
subject_name::list_cert_dns_names(self)
191+
}
177192
}

src/subject_name/dns_name.rs

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,18 @@
1414

1515
#[cfg(feature = "alloc")]
1616
use alloc::string::String;
17+
use core::fmt::Write;
1718

1819
/// A DNS Name suitable for use in the TLS Server Name Indication (SNI)
1920
/// extension and/or for use as the reference hostname for which to verify a
2021
/// certificate.
2122
///
2223
/// A `DnsName` is guaranteed to be syntactically valid. The validity rules are
2324
/// specified in [RFC 5280 Section 7.2], except that underscores are also
24-
/// allowed.
25+
/// allowed. `DnsName`s do not include wildcard labels.
2526
///
2627
/// `DnsName` stores a copy of the input it was constructed from in a `String`
27-
/// and so it is only available when the `std` default feature is enabled.
28-
///
29-
/// `Eq`, `PartialEq`, etc. are not implemented because name comparison
30-
/// frequently should be done case-insensitively and/or with other caveats that
31-
/// depend on the specific circumstances in which the comparison is done.
28+
/// and so it is only available when the `alloc` default feature is enabled.
3229
///
3330
/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2
3431
///
@@ -69,14 +66,10 @@ impl From<DnsNameRef<'_>> for DnsName {
6966
///
7067
/// A `DnsNameRef` is guaranteed to be syntactically valid. The validity rules
7168
/// are specified in [RFC 5280 Section 7.2], except that underscores are also
72-
/// allowed.
73-
///
74-
/// `Eq`, `PartialEq`, etc. are not implemented because name comparison
75-
/// frequently should be done case-insensitively and/or with other caveats that
76-
/// depend on the specific circumstances in which the comparison is done.
69+
/// allowed. `DnsNameRef`s do not include wildcard labels.
7770
///
7871
/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2
79-
#[derive(Clone, Copy)]
72+
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
8073
pub struct DnsNameRef<'a>(pub(crate) &'a [u8]);
8174

8275
impl AsRef<[u8]> for DnsNameRef<'_> {
@@ -105,7 +98,11 @@ impl<'a> DnsNameRef<'a> {
10598
/// Constructs a `DnsNameRef` from the given input if the input is a
10699
/// syntactically-valid DNS name.
107100
pub fn try_from_ascii(dns_name: &'a [u8]) -> Result<Self, InvalidDnsNameError> {
108-
if !is_valid_reference_dns_id(untrusted::Input::from(dns_name)) {
101+
if !is_valid_dns_id(
102+
untrusted::Input::from(dns_name),
103+
IdRole::Reference,
104+
AllowWildcards::No,
105+
) {
109106
return Err(InvalidDnsNameError);
110107
}
111108

@@ -130,19 +127,18 @@ impl<'a> DnsNameRef<'a> {
130127
}
131128
}
132129

133-
/// Requires the `alloc` feature.
134-
#[cfg(feature = "alloc")]
135130
impl core::fmt::Debug for DnsNameRef<'_> {
136131
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
137-
let lowercase = self.clone().to_owned();
138-
f.debug_tuple("DnsNameRef").field(&lowercase.0).finish()
139-
}
140-
}
132+
f.write_str("DnsNameRef(\"")?;
141133

142-
#[cfg(not(feature = "alloc"))]
143-
impl core::fmt::Debug for DnsNameRef<'_> {
144-
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
145-
f.debug_tuple("DnsNameRef").field(&self.0).finish()
134+
// Convert each byte of the underlying ASCII string to a `char` and
135+
// downcase it prior to formatting it. We avoid self.clone().to_owned()
136+
// since it requires allocation.
137+
for &ch in self.0 {
138+
f.write_char(char::from(ch).to_ascii_lowercase())?;
139+
}
140+
141+
f.write_str("\")")
146142
}
147143
}
148144

@@ -154,6 +150,94 @@ impl<'a> From<DnsNameRef<'a>> for &'a str {
154150
}
155151
}
156152

153+
/// A DNS name that may be either a DNS name identifier presented by a server (which may include
154+
/// wildcards), or a DNS name identifier referenced by a client for matching purposes (wildcards
155+
/// not permitted).
156+
pub enum GeneralDnsNameRef<'name> {
157+
/// a reference to a DNS name that may be used for matching purposes.
158+
DnsName(DnsNameRef<'name>),
159+
/// a reference to a presented DNS name that may include a wildcard.
160+
Wildcard(WildcardDnsNameRef<'name>),
161+
}
162+
163+
impl<'a> From<GeneralDnsNameRef<'a>> for &'a str {
164+
fn from(d: GeneralDnsNameRef<'a>) -> Self {
165+
match d {
166+
GeneralDnsNameRef::DnsName(name) => name.into(),
167+
GeneralDnsNameRef::Wildcard(name) => name.into(),
168+
}
169+
}
170+
}
171+
172+
/// A reference to a DNS Name presented by a server that may include a wildcard.
173+
///
174+
/// A `WildcardDnsNameRef` is guaranteed to be syntactically valid. The validity rules
175+
/// are specified in [RFC 5280 Section 7.2], except that underscores are also
176+
/// allowed.
177+
///
178+
/// Additionally, while [RFC6125 Section 4.1] says that a wildcard label may be of the form
179+
/// `<x>*<y>.<DNSID>`, where `<x>` and/or `<y>` may be empty, we follow a stricter policy common
180+
/// to most validation libraries (e.g. NSS) and only accept wildcard labels that are exactly `*`.
181+
///
182+
/// [RFC 5280 Section 7.2]: https://tools.ietf.org/html/rfc5280#section-7.2
183+
/// [RFC 6125 Section 4.1]: https://www.rfc-editor.org/rfc/rfc6125#section-4.1
184+
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
185+
pub struct WildcardDnsNameRef<'a>(&'a [u8]);
186+
187+
impl<'a> WildcardDnsNameRef<'a> {
188+
/// Constructs a `WildcardDnsNameRef` from the given input if the input is a
189+
/// syntactically-valid DNS name.
190+
pub fn try_from_ascii(dns_name: &'a [u8]) -> Result<Self, InvalidDnsNameError> {
191+
if !is_valid_dns_id(
192+
untrusted::Input::from(dns_name),
193+
IdRole::Reference,
194+
AllowWildcards::Yes,
195+
) {
196+
return Err(InvalidDnsNameError);
197+
}
198+
199+
Ok(Self(dns_name))
200+
}
201+
202+
/// Constructs a `WildcardDnsNameRef` from the given input if the input is a
203+
/// syntactically-valid DNS name.
204+
pub fn try_from_ascii_str(dns_name: &'a str) -> Result<Self, InvalidDnsNameError> {
205+
Self::try_from_ascii(dns_name.as_bytes())
206+
}
207+
}
208+
209+
impl core::fmt::Debug for WildcardDnsNameRef<'_> {
210+
fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
211+
f.write_str("WildcardDnsNameRef(\"")?;
212+
213+
// Convert each byte of the underlying ASCII string to a `char` and
214+
// downcase it prior to formatting it. We avoid self.to_owned() since
215+
// it requires allocation.
216+
for &ch in self.0 {
217+
f.write_char(char::from(ch).to_ascii_lowercase())?;
218+
}
219+
220+
f.write_str("\")")
221+
}
222+
}
223+
224+
impl<'a> From<WildcardDnsNameRef<'a>> for &'a str {
225+
fn from(WildcardDnsNameRef(d): WildcardDnsNameRef<'a>) -> Self {
226+
// The unwrap won't fail because WildcardDnsNameRef are guaranteed to be ASCII
227+
// and ASCII is a subset of UTF-8.
228+
core::str::from_utf8(d).unwrap()
229+
}
230+
}
231+
232+
impl AsRef<str> for WildcardDnsNameRef<'_> {
233+
#[inline]
234+
fn as_ref(&self) -> &str {
235+
// The unwrap won't fail because WildcardDnsNameRef are guaranteed to be ASCII
236+
// and ASCII is a subset of UTF-8.
237+
core::str::from_utf8(self.0).unwrap()
238+
}
239+
}
240+
157241
pub(super) fn presented_id_matches_reference_id(
158242
presented_dns_id: untrusted::Input,
159243
reference_dns_id: untrusted::Input,
@@ -445,10 +529,6 @@ enum IdRole {
445529
NameConstraint,
446530
}
447531

448-
fn is_valid_reference_dns_id(hostname: untrusted::Input) -> bool {
449-
is_valid_dns_id(hostname, IdRole::Reference, AllowWildcards::No)
450-
}
451-
452532
// https://tools.ietf.org/html/rfc5280#section-4.2.1.6:
453533
//
454534
// When the subjectAltName extension contains a domain name system

src/subject_name/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1414

1515
mod dns_name;
16+
#[cfg(feature = "alloc")]
17+
pub(crate) use dns_name::GeneralDnsNameRef;
1618
pub use dns_name::{DnsNameRef, InvalidDnsNameError};
1719

1820
/// Requires the `alloc` feature.
@@ -29,6 +31,8 @@ pub use ip_address::{AddrParseError, IpAddrRef};
2931
pub use ip_address::IpAddr;
3032

3133
mod verify;
34+
#[cfg(feature = "alloc")]
35+
pub(super) use verify::list_cert_dns_names;
3236
pub(super) use verify::{
3337
check_name_constraints, verify_cert_subject_name, SubjectCommonNameContents,
3438
};

src/subject_name/verify.rs

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ use crate::{
2121
cert::{Cert, EndEntityOrCa},
2222
der, Error,
2323
};
24+
#[cfg(feature = "alloc")]
25+
use {
26+
alloc::vec::Vec,
27+
dns_name::{GeneralDnsNameRef, WildcardDnsNameRef},
28+
};
2429

2530
pub(crate) fn verify_cert_dns_name(
2631
cert: &crate::EndEntityCert,
@@ -33,7 +38,7 @@ pub(crate) fn verify_cert_dns_name(
3338
cert.subject_alt_name,
3439
SubjectCommonNameContents::Ignore,
3540
Err(Error::CertNotValidForName),
36-
&|name| {
41+
&mut |name| {
3742
if let GeneralName::DnsName(presented_id) = name {
3843
match dns_name::presented_id_matches_reference_id(presented_id, dns_name) {
3944
Some(true) => return NameIteration::Stop(Ok(())),
@@ -67,7 +72,7 @@ pub(crate) fn verify_cert_subject_name(
6772
cert.inner().subject_alt_name,
6873
SubjectCommonNameContents::Ignore,
6974
Err(Error::CertNotValidForName),
70-
&|name| {
75+
&mut |name| {
7176
if let GeneralName::IpAddress(presented_id) = name {
7277
match ip_address::presented_id_matches_reference_id(presented_id, ip_address) {
7378
Ok(true) => return NameIteration::Stop(Ok(())),
@@ -115,7 +120,7 @@ pub(crate) fn check_name_constraints(
115120
child.subject_alt_name,
116121
subject_common_name_contents,
117122
Ok(()),
118-
&|name| {
123+
&mut |name| {
119124
check_presented_id_conforms_to_constraints(
120125
name,
121126
permitted_subtrees,
@@ -302,12 +307,12 @@ pub(crate) enum SubjectCommonNameContents {
302307
Ignore,
303308
}
304309

305-
fn iterate_names(
306-
subject: Option<untrusted::Input>,
307-
subject_alt_name: Option<untrusted::Input>,
310+
fn iterate_names<'names>(
311+
subject: Option<untrusted::Input<'names>>,
312+
subject_alt_name: Option<untrusted::Input<'names>>,
308313
subject_common_name_contents: SubjectCommonNameContents,
309314
result_if_never_stopped_early: Result<(), Error>,
310-
f: &dyn Fn(GeneralName) -> NameIteration,
315+
f: &mut impl FnMut(GeneralName<'names>) -> NameIteration,
311316
) -> Result<(), Error> {
312317
if let Some(subject_alt_name) = subject_alt_name {
313318
let mut subject_alt_name = untrusted::Reader::new(subject_alt_name);
@@ -351,6 +356,39 @@ fn iterate_names(
351356
}
352357
}
353358

359+
#[cfg(feature = "alloc")]
360+
pub(crate) fn list_cert_dns_names<'names>(
361+
cert: &'names crate::EndEntityCert<'names>,
362+
) -> Result<impl Iterator<Item = GeneralDnsNameRef<'names>>, Error> {
363+
let cert = &cert.inner();
364+
let mut names = Vec::new();
365+
366+
iterate_names(
367+
Some(cert.subject),
368+
cert.subject_alt_name,
369+
SubjectCommonNameContents::DnsName,
370+
Ok(()),
371+
&mut |name| {
372+
if let GeneralName::DnsName(presented_id) = name {
373+
let dns_name = DnsNameRef::try_from_ascii(presented_id.as_slice_less_safe())
374+
.map(GeneralDnsNameRef::DnsName)
375+
.or_else(|_| {
376+
WildcardDnsNameRef::try_from_ascii(presented_id.as_slice_less_safe())
377+
.map(GeneralDnsNameRef::Wildcard)
378+
});
379+
380+
// if the name could be converted to a DNS name, add it; otherwise,
381+
// keep going.
382+
if let Ok(name) = dns_name {
383+
names.push(name)
384+
}
385+
}
386+
NameIteration::KeepGoing
387+
},
388+
)
389+
.map(|_| names.into_iter())
390+
}
391+
354392
// It is *not* valid to derive `Eq`, `PartialEq, etc. for this type. In
355393
// particular, for the types of `GeneralName`s that we don't understand, we
356394
// don't even store the value. Also, the meaning of a `GeneralName` in a name

0 commit comments

Comments
 (0)