Skip to content

Commit 7bc8a7a

Browse files
committed
Add context in name validation errors
1 parent 4143e95 commit 7bc8a7a

File tree

6 files changed

+318
-60
lines changed

6 files changed

+318
-60
lines changed

src/error.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,16 @@
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 alloc::string::String;
17+
#[cfg(feature = "alloc")]
18+
use alloc::vec::Vec;
1519
use core::fmt;
1620
use core::ops::ControlFlow;
1721

22+
#[cfg(feature = "alloc")]
23+
use pki_types::ServerName;
24+
1825
/// An error that occurs during certificate validation or name validation.
1926
#[derive(Clone, Debug, PartialEq, Eq)]
2027
#[non_exhaustive]
@@ -32,9 +39,6 @@ pub enum Error {
3239
/// later than the certificate's notAfter time.
3340
CertExpired,
3441

35-
/// The certificate is not valid for the name it is being validated for.
36-
CertNotValidForName,
37-
3842
/// The certificate is not valid yet; i.e. the time it is being validated
3943
/// for is earlier than the certificate's notBefore time.
4044
CertNotValidYet,
@@ -127,6 +131,26 @@ pub enum Error {
127131
/// Trailing data was found while parsing DER-encoded input for the named type.
128132
TrailingData(DerTypeId),
129133

134+
/// The certificate is not valid for the name it is being validated for.
135+
///
136+
/// Variant without context for use in no-`alloc` environments. See `UnexpectedCertName` for
137+
/// the variant that is used in `alloc` environments.
138+
UnexpectedCertNameSimple,
139+
140+
/// The certificate is not valid for the name it is being validated for.
141+
///
142+
/// Variant with context for use in `alloc` environments. See `UnexpectedCertNameSimple`
143+
/// for the variant that is used in no-`alloc` environments.
144+
#[cfg(feature = "alloc")]
145+
UnexpectedCertName {
146+
/// Expected server name.
147+
expected: ServerName<'static>,
148+
/// The names presented in the end entity certificate.
149+
///
150+
/// Unlike `expected`, these names might contain wildcard labels.
151+
presented: Vec<String>,
152+
},
153+
130154
/// A valid issuer for the certificate could not be found.
131155
UnknownIssuer,
132156

@@ -216,7 +240,6 @@ impl Error {
216240
match &self {
217241
// Errors related to certificate validity
218242
Self::CertNotValidYet | Self::CertExpired => 290,
219-
Self::CertNotValidForName => 280,
220243
Self::CertRevoked | Self::UnknownRevocationStatus | Self::CrlExpired => 270,
221244
Self::InvalidCrlSignatureForPublicKey | Self::InvalidSignatureForPublicKey => 260,
222245
Self::SignatureAlgorithmMismatch => 250,
@@ -225,6 +248,9 @@ impl Error {
225248
Self::PathLenConstraintViolated => 220,
226249
Self::CaUsedAsEndEntity | Self::EndEntityUsedAsCa => 210,
227250
Self::IssuerNotCrlSigner => 200,
251+
Self::UnexpectedCertNameSimple => 280,
252+
#[cfg(feature = "alloc")]
253+
Self::UnexpectedCertName { .. } => 280,
228254

229255
// Errors related to supported features used in an invalid way.
230256
Self::InvalidCertValidity => 190,

src/subject_name/dns_name.rs

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,56 @@
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 alloc::format;
1517
use core::fmt::Write;
1618

19+
#[cfg(feature = "alloc")]
20+
use pki_types::ServerName;
1721
use pki_types::{DnsName, InvalidDnsNameError};
1822

1923
use super::verify::{GeneralName, NameIterator};
2024
use crate::{Cert, Error};
2125

2226
pub(crate) fn verify_dns_names(reference: &DnsName<'_>, cert: &Cert<'_>) -> Result<(), Error> {
2327
let dns_name = untrusted::Input::from(reference.as_ref().as_bytes());
24-
NameIterator::new(Some(cert.subject), cert.subject_alt_name)
25-
.find_map(|result| {
26-
let name = match result {
27-
Ok(name) => name,
28-
Err(err) => return Some(Err(err)),
29-
};
28+
let result = NameIterator::new(Some(cert.subject), cert.subject_alt_name).find_map(|result| {
29+
let name = match result {
30+
Ok(name) => name,
31+
Err(err) => return Some(Err(err)),
32+
};
3033

31-
let presented_id = match name {
32-
GeneralName::DnsName(presented) => presented,
33-
_ => return None,
34-
};
34+
let presented_id = match name {
35+
GeneralName::DnsName(presented) => presented,
36+
_ => return None,
37+
};
3538

36-
match presented_id_matches_reference_id(presented_id, IdRole::Reference, dns_name) {
37-
Ok(true) => Some(Ok(())),
38-
Ok(false) | Err(Error::MalformedDnsIdentifier) => None,
39-
Err(e) => Some(Err(e)),
40-
}
39+
match presented_id_matches_reference_id(presented_id, IdRole::Reference, dns_name) {
40+
Ok(true) => Some(Ok(())),
41+
Ok(false) | Err(Error::MalformedDnsIdentifier) => None,
42+
Err(e) => Some(Err(e)),
43+
}
44+
});
45+
46+
match result {
47+
Some(result) => return result,
48+
#[cfg(feature = "alloc")]
49+
None => {}
50+
#[cfg(not(feature = "alloc"))]
51+
None => return Err(Error::UnexpectedCertNameSimple),
52+
}
53+
54+
// Try to yield a more useful error. To avoid allocating on the happy path,
55+
// we reconstruct the same `NameIterator` and replay it.
56+
#[cfg(feature = "alloc")]
57+
{
58+
Err(Error::UnexpectedCertName {
59+
expected: ServerName::DnsName(reference.to_owned()),
60+
presented: NameIterator::new(Some(cert.subject), cert.subject_alt_name)
61+
.filter_map(|result| Some(format!("{:?}", result.ok()?)))
62+
.collect(),
4163
})
42-
.unwrap_or(Err(Error::CertNotValidForName))
64+
}
4365
}
4466

4567
/// A reference to a DNS Name presented by a server that may include a wildcard.

src/subject_name/ip_address.rs

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
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 alloc::format;
17+
1518
use pki_types::IpAddr;
19+
#[cfg(feature = "alloc")]
20+
use pki_types::ServerName;
1621

1722
use super::verify::{GeneralName, NameIterator};
1823
use crate::{Cert, Error};
@@ -23,24 +28,40 @@ pub(crate) fn verify_ip_address_names(reference: &IpAddr, cert: &Cert<'_>) -> Re
2328
IpAddr::V6(ip) => untrusted::Input::from(ip.as_ref()),
2429
};
2530

26-
NameIterator::new(None, cert.subject_alt_name)
27-
.find_map(|result| {
28-
let name = match result {
29-
Ok(name) => name,
30-
Err(err) => return Some(Err(err)),
31-
};
31+
let result = NameIterator::new(None, cert.subject_alt_name).find_map(|result| {
32+
let name = match result {
33+
Ok(name) => name,
34+
Err(err) => return Some(Err(err)),
35+
};
3236

33-
let presented_id = match name {
34-
GeneralName::IpAddress(presented) => presented,
35-
_ => return None,
36-
};
37+
let presented_id = match name {
38+
GeneralName::IpAddress(presented) => presented,
39+
_ => return None,
40+
};
3741

38-
match presented_id_matches_reference_id(presented_id, ip_address) {
39-
true => Some(Ok(())),
40-
false => None,
41-
}
42+
match presented_id_matches_reference_id(presented_id, ip_address) {
43+
true => Some(Ok(())),
44+
false => None,
45+
}
46+
});
47+
48+
match result {
49+
Some(result) => return result,
50+
#[cfg(feature = "alloc")]
51+
None => {}
52+
#[cfg(not(feature = "alloc"))]
53+
None => Err(Error::UnexpectedCertNameSimple),
54+
}
55+
56+
#[cfg(feature = "alloc")]
57+
{
58+
Err(Error::UnexpectedCertName {
59+
expected: ServerName::from(*reference),
60+
presented: NameIterator::new(None, cert.subject_alt_name)
61+
.filter_map(|result| Some(format!("{:?}", result.ok()?)))
62+
.collect(),
4263
})
43-
.unwrap_or(Err(Error::CertNotValidForName))
64+
}
4465
}
4566

4667
// https://tools.ietf.org/html/rfc5280#section-4.2.1.6 says:

src/subject_name/verify.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
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 alloc::string::String;
17+
#[cfg(feature = "alloc")]
18+
use core::fmt;
19+
1520
use super::dns_name::{self, IdRole};
1621
use super::ip_address;
1722
use crate::der::{self, FromDer};
@@ -295,3 +300,136 @@ impl<'a> FromDer<'a> for GeneralName<'a> {
295300

296301
const TYPE_ID: DerTypeId = DerTypeId::GeneralName;
297302
}
303+
304+
#[cfg(feature = "alloc")]
305+
impl fmt::Debug for GeneralName<'_> {
306+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307+
match self {
308+
GeneralName::DnsName(name) => write!(
309+
f,
310+
"DnsName(\"{}\")",
311+
String::from_utf8_lossy(name.as_slice_less_safe())
312+
),
313+
GeneralName::DirectoryName => write!(f, "DirectoryName"),
314+
GeneralName::IpAddress(ip) => {
315+
write!(f, "IpAddress({:?})", IpAddrSlice(ip.as_slice_less_safe()))
316+
}
317+
GeneralName::UniformResourceIdentifier(uri) => write!(
318+
f,
319+
"UniformResourceIdentifier(\"{}\")",
320+
String::from_utf8_lossy(uri.as_slice_less_safe())
321+
),
322+
GeneralName::Unsupported(tag) => write!(f, "Unsupported({})", tag),
323+
}
324+
}
325+
}
326+
327+
#[cfg(feature = "alloc")]
328+
struct IpAddrSlice<'a>(&'a [u8]);
329+
330+
#[cfg(feature = "alloc")]
331+
impl fmt::Debug for IpAddrSlice<'_> {
332+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333+
match self.0.len() {
334+
4 => {
335+
let mut first = true;
336+
for byte in self.0 {
337+
match first {
338+
true => first = false,
339+
false => f.write_str(".")?,
340+
}
341+
342+
write!(f, "{}", byte)?;
343+
}
344+
345+
Ok(())
346+
}
347+
16 => {
348+
let mut first = true;
349+
for group in self.0.chunks_exact(2) {
350+
match first {
351+
true => first = false,
352+
false => f.write_str(":")?,
353+
}
354+
355+
match group {
356+
[0, 0] => f.write_str("0")?,
357+
_ => write!(f, "{:02x}{:02x}", group[0], group[1])?,
358+
}
359+
}
360+
Ok(())
361+
}
362+
_ => {
363+
f.write_str("[invalid: ")?;
364+
let mut first = true;
365+
for byte in self.0 {
366+
match first {
367+
true => first = false,
368+
false => f.write_str(", ")?,
369+
}
370+
write!(f, "{:02x}", byte)?;
371+
}
372+
f.write_str("]")
373+
}
374+
}
375+
}
376+
}
377+
378+
#[cfg(all(test, feature = "alloc"))]
379+
mod tests {
380+
use super::*;
381+
382+
#[test]
383+
fn debug_names() {
384+
assert_eq!(
385+
format!(
386+
"{:?}",
387+
GeneralName::DnsName(untrusted::Input::from(b"example.com"))
388+
),
389+
"DnsName(\"example.com\")"
390+
);
391+
392+
assert_eq!(format!("{:?}", GeneralName::DirectoryName), "DirectoryName");
393+
394+
assert_eq!(
395+
format!(
396+
"{:?}",
397+
GeneralName::IpAddress(untrusted::Input::from(&[192, 0, 2, 1][..]))
398+
),
399+
"IpAddress(192.0.2.1)"
400+
);
401+
402+
assert_eq!(
403+
format!(
404+
"{:?}",
405+
GeneralName::IpAddress(untrusted::Input::from(
406+
&[0x20, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0d, 0xb8][..]
407+
))
408+
),
409+
"IpAddress(2001:0:0:0:0:0:0:0db8)"
410+
);
411+
412+
assert_eq!(
413+
format!(
414+
"{:?}",
415+
GeneralName::IpAddress(untrusted::Input::from(&[1, 2, 3, 4, 5, 6][..]))
416+
),
417+
"IpAddress([invalid: 01, 02, 03, 04, 05, 06])"
418+
);
419+
420+
assert_eq!(
421+
format!(
422+
"{:?}",
423+
GeneralName::UniformResourceIdentifier(untrusted::Input::from(
424+
b"https://example.com"
425+
))
426+
),
427+
"UniformResourceIdentifier(\"https://example.com\")"
428+
);
429+
430+
assert_eq!(
431+
format!("{:?}", GeneralName::Unsupported(0x42)),
432+
"Unsupported(66)"
433+
);
434+
}
435+
}

tests/generate.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,16 @@ def generate_tls_server_cert_test(
286286

287287
valid_names_str: str = ", ".join('"' + name + '"' for name in valid_names)
288288
invalid_names_str: str = ", ".join('"' + name + '"' for name in invalid_names)
289+
presented_names_str: str = ""
290+
for san in sans if sans is not None else []:
291+
if presented_names_str:
292+
presented_names_str += ", "
293+
if isinstance(san, x509.DNSName):
294+
presented_names_str += f'"DnsName(\\"{san.value}\\")"'
295+
elif isinstance(san, x509.IPAddress):
296+
presented_names_str += f'"IpAddress({san.value})"'
297+
else:
298+
raise ValueError(f"Unsupported SAN type: {type(san)}")
289299

290300
print(
291301
"""
@@ -294,7 +304,7 @@ def generate_tls_server_cert_test(
294304
let ee = include_bytes!("%(ee_cert_path)s");
295305
let ca = include_bytes!("%(ca_cert_path)s");
296306
assert_eq!(
297-
check_cert(ee, ca, &[%(valid_names_str)s], &[%(invalid_names_str)s]),
307+
check_cert(ee, ca, &[%(valid_names_str)s], &[%(invalid_names_str)s], &[%(presented_names_str)s)]),
298308
%(expected)s
299309
);
300310
}"""

0 commit comments

Comments
 (0)