From f7378e24be4b9a113e7f80c8d1a50b2fc6a9f811 Mon Sep 17 00:00:00 2001 From: "alex.berger@nexiot.ch" Date: Wed, 11 Mar 2020 22:02:02 +0100 Subject: [PATCH 1/2] - First draw with support for IPv4, IPv6 and DNS names and DNS wildcards in SAN and CN. --- src/name.rs | 8 +- src/san.rs | 157 +++++++++++++++++++++++++++++ src/webpki.rs | 4 + tests/san.rs | 54 ++++++++++ tests/san/dns_in_cn.der | Bin 0 -> 784 bytes tests/san/dns_in_cn.json | 7 ++ tests/san/dns_in_san.der | Bin 0 -> 800 bytes tests/san/dns_in_san.json | 10 ++ tests/san/dns_wildcard_in_cn.der | Bin 0 -> 788 bytes tests/san/dns_wildcard_in_cn.json | 7 ++ tests/san/dns_wildcard_in_san.der | Bin 0 -> 802 bytes tests/san/dns_wildcard_in_san.json | 10 ++ tests/san/ip_in_cn.der | Bin 0 -> 780 bytes tests/san/ip_in_cn.json | 7 ++ tests/san/ip_in_san.der | Bin 0 -> 793 bytes tests/san/ip_in_san.json | 10 ++ 16 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 src/san.rs create mode 100644 tests/san.rs create mode 100644 tests/san/dns_in_cn.der create mode 100644 tests/san/dns_in_cn.json create mode 100644 tests/san/dns_in_san.der create mode 100644 tests/san/dns_in_san.json create mode 100644 tests/san/dns_wildcard_in_cn.der create mode 100644 tests/san/dns_wildcard_in_cn.json create mode 100644 tests/san/dns_wildcard_in_san.der create mode 100644 tests/san/dns_wildcard_in_san.json create mode 100644 tests/san/ip_in_cn.der create mode 100644 tests/san/ip_in_cn.json create mode 100644 tests/san/ip_in_san.der create mode 100644 tests/san/ip_in_san.json diff --git a/src/name.rs b/src/name.rs index 24cb69f0..0076e546 100644 --- a/src/name.rs +++ b/src/name.rs @@ -379,12 +379,12 @@ fn presented_ip_address_matches_constraint( } #[derive(Clone, Copy)] -enum NameIteration { +pub(crate) enum NameIteration { KeepGoing, Stop(Result<(), Error>), } -fn iterate_names( +pub(crate) fn iterate_names( subject: untrusted::Input, subject_alt_name: Option, result_if_never_stopped_early: Result<(), Error>, f: &dyn Fn(GeneralName) -> NameIteration, ) -> Result<(), Error> { @@ -422,7 +422,7 @@ fn iterate_names( // constraint is different than the meaning of the identically-represented // `GeneralName` in other contexts. #[derive(Clone, Copy)] -enum GeneralName<'a> { +pub(crate) enum GeneralName<'a> { DNSName(untrusted::Input<'a>), DirectoryName(untrusted::Input<'a>), IPAddress(untrusted::Input<'a>), @@ -463,7 +463,7 @@ fn general_name<'a>(input: &mut untrusted::Reader<'a>) -> Result Ok(name) } -fn presented_dns_id_matches_reference_dns_id( +pub(crate) fn presented_dns_id_matches_reference_dns_id( presented_dns_id: untrusted::Input, reference_dns_id: untrusted::Input, ) -> Option { presented_dns_id_matches_reference_dns_id_internal( diff --git a/src/san.rs b/src/san.rs new file mode 100644 index 00000000..e03277b3 --- /dev/null +++ b/src/san.rs @@ -0,0 +1,157 @@ +use crate::{ + der::read_tag_and_get_value, + name::{iterate_names, presented_dns_id_matches_reference_dns_id, GeneralName, NameIteration}, + Error, +}; +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; + +/// Subject Alternative Name (SAN) +#[non_exhaustive] +pub enum SubjectAlternativeName { + /// DNS name + Dns(String), + /// IPv4 or IPv6 address + Ip(IpAddr), +} + +impl SubjectAlternativeName { + const OID_CN: [u8; 3] = [85, 4, 3]; + + fn traverse<'a>( + input: &'a untrusted::Input, agg: &mut Vec<(u8, Vec)>, + ) -> Result<(), Error> { + let mut reader = untrusted::Reader::new(input.clone()); + while let Ok((tag, value)) = read_tag_and_get_value(&mut reader) { + agg.push((tag, value.as_slice_less_safe().to_vec())); + Self::traverse(&value.clone(), agg)?; + } + Ok(()) + } + + /// Strings in Rust are unicode (UTF-8), and unicode codepoints are a + /// superset of iso-8859-1 characters. This specific conversion is + /// actually trivial. + fn latin1_to_string(s: &[u8]) -> String { + s.iter().map(|&c| c as char).collect() + } + + fn extract_common_name(der: &untrusted::Input) -> Option { + let mut input = vec![]; + Self::traverse(der, &mut input).unwrap(); + if let Some(oid_position) = input + .iter() + .position(|(tag, value)| *tag == 6u8 && value.as_slice() == Self::OID_CN) + { + match input.get(oid_position + 1) { + // PrintableString (Subset of ASCII, therefore valid UTF8) + Some((19u8, value)) => String::from_utf8(value.clone()).ok(), + // UTF8String + Some((12u8, value)) => String::from_utf8(value.clone()).ok(), + // UniversalString (UCS-4 32-bit encoded) + // Some((28u8, value)) => unimplemented!(), + // BMPString (UCS-2 16-bit encoded) + //Some((30u8, value)) => unimplemented!(), + // VideotexString resp. TeletexString ISO-8859-1 encoded + Some((21u8, value)) => Some(Self::latin1_to_string(value.as_slice())), + _ => None, + } + } else { + None + } + } + + fn matches_dns(dns: &str, name: &GeneralName) -> bool { + let dns_input = untrusted::Input::from(dns.as_bytes()); + match name { + GeneralName::DNSName(d) => { + presented_dns_id_matches_reference_dns_id(d.clone(), dns_input).unwrap_or(false) + } + GeneralName::DirectoryName(d) => { + if let Some(x) = Self::extract_common_name(d) { + //x == dns + presented_dns_id_matches_reference_dns_id( + untrusted::Input::from(x.as_bytes()), + dns_input, + ) + .unwrap_or(false) + } else { + false + } + } + _ => false, + } + } + + fn matches_ip(ip: &IpAddr, name: &GeneralName) -> Result { + match name { + GeneralName::IPAddress(d) => match ip { + IpAddr::V4(v4) if d.len() == 4 => { + let mut reader = untrusted::Reader::new(d.clone()); + let a = reader.read_byte()?; + let b = reader.read_byte()?; + let c = reader.read_byte()?; + let d = reader.read_byte()?; + Ok(Ipv4Addr::from([a, b, c, d]) == *v4) + } + IpAddr::V6(v6) if d.len() == 16 => { + let mut reader = untrusted::Reader::new(d.clone()); + let a = reader.read_byte()?; + let b = reader.read_byte()?; + let c = reader.read_byte()?; + let d = reader.read_byte()?; + let e = reader.read_byte()?; + let f = reader.read_byte()?; + let g = reader.read_byte()?; + let h = reader.read_byte()?; + let i = reader.read_byte()?; + let j = reader.read_byte()?; + let k = reader.read_byte()?; + let l = reader.read_byte()?; + let m = reader.read_byte()?; + let n = reader.read_byte()?; + let o = reader.read_byte()?; + let p = reader.read_byte()?; + Ok(Ipv6Addr::from([a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]) == *v6) + } + _ => Ok(false), + }, + GeneralName::DirectoryName(d) => { + if let Some(x) = Self::extract_common_name(d) { + match IpAddr::from_str(x.as_str()) { + Ok(a) => Ok(a == *ip), + Err(_) => Ok(false), + } + } else { + Ok(false) + } + } + _ => Ok(false), + } + } + + fn matches(&self, _cert: &super::EndEntityCert, name: &GeneralName) -> Result { + match self { + SubjectAlternativeName::Dns(d) => Ok(Self::matches_dns(d, name)), + SubjectAlternativeName::Ip(ip) => Self::matches_ip(ip, name).map_err(|_| Error::BadDER), + //_ => Ok(false), + } + } + + /// Check if this name is the subject of the provided certificate. + pub fn is_subject_of(&self, cert: &super::EndEntityCert) -> Result<(), Error> { + let crt = &cert.inner; + iterate_names( + crt.subject, + crt.subject_alt_name, + Err(Error::CertNotValidForName), + &|name| match self.matches(cert, &name) { + Ok(true) => NameIteration::Stop(Ok(())), + Ok(false) => NameIteration::KeepGoing, + Err(e) => NameIteration::Stop(Err(e)), + }, + ) + } +} diff --git a/src/webpki.rs b/src/webpki.rs index adcded53..32269159 100644 --- a/src/webpki.rs +++ b/src/webpki.rs @@ -51,6 +51,8 @@ mod calendar; mod cert; mod error; mod name; +#[cfg(feature = "std")] +mod san; mod signed_data; mod time; @@ -61,6 +63,8 @@ mod verify_cert; pub use error::Error; pub use name::{DNSNameRef, InvalidDNSNameError}; +#[cfg(feature = "std")] +pub use san::SubjectAlternativeName; #[cfg(feature = "std")] pub use name::DNSName; diff --git a/tests/san.rs b/tests/san.rs new file mode 100644 index 00000000..67c76823 --- /dev/null +++ b/tests/san.rs @@ -0,0 +1,54 @@ +#[cfg(feature = "std")] +mod tests { + + use std::net::{IpAddr, Ipv4Addr}; + use webpki::SubjectAlternativeName; + + #[test] + fn dns_in_cn() { + let der = include_bytes!("san/dns_in_cn.der"); + let cert = webpki::EndEntityCert::from(der).unwrap(); + let name = SubjectAlternativeName::Dns("example.com".to_string()); + assert_eq!(name.is_subject_of(&cert), Ok(())); + } + + #[test] + fn dns_wildcard_in_cn() { + let der = include_bytes!("san/dns_wildcard_in_cn.der"); + let cert = webpki::EndEntityCert::from(der).unwrap(); + let name = SubjectAlternativeName::Dns("sub.example.com".to_string()); + assert_eq!(name.is_subject_of(&cert), Ok(())); + } + + #[test] + fn dns_in_san() { + let der = include_bytes!("san/dns_in_san.der"); + let cert = webpki::EndEntityCert::from(der).unwrap(); + let name = SubjectAlternativeName::Dns("example.org".to_string()); + assert_eq!(name.is_subject_of(&cert), Ok(())); + } + + #[test] + fn dns_wildcard_in_san() { + let der = include_bytes!("san/dns_wildcard_in_san.der"); + let cert = webpki::EndEntityCert::from(der).unwrap(); + let name = SubjectAlternativeName::Dns("sub.example.org".to_string()); + assert_eq!(name.is_subject_of(&cert), Ok(())); + } + + #[test] + fn ip_in_cn() { + let der = include_bytes!("san/ip_in_cn.der"); + let cert = webpki::EndEntityCert::from(der).unwrap(); + let name = SubjectAlternativeName::Ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + assert_eq!(name.is_subject_of(&cert), Ok(())); + } + + #[test] + fn ip_in_san() { + let der = include_bytes!("san/ip_in_san.der"); + let cert = webpki::EndEntityCert::from(der).unwrap(); + let name = SubjectAlternativeName::Ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + assert_eq!(name.is_subject_of(&cert), Ok(())); + } +} diff --git a/tests/san/dns_in_cn.der b/tests/san/dns_in_cn.der new file mode 100644 index 0000000000000000000000000000000000000000..6708846b55e01f68475b8d6d8720b6872376d28f GIT binary patch literal 784 zcmXqLV&*YuV*IjznTe5!iNh{qdR&S-1B(GK8>d#AN85K^Mn-N{1_Loe5d$GM=1>-9 zVeZt5#N2|MRK4Wisl8Zr)n$5%(Co=x0cl$^eq4V4 zW9y5>+gTacer;KwB0qb6PwRtONk1Z@rivdC)@JU zu3zml74HXL^WB|N{BD7ydWpH_LA^CM>fQ+5YrcEMI4i)@Nv5hTX4O{RxtpRVXMdSA zb!VTmknc)QgNOT5Dn(2`FWF{!phuAZV(l6m^Y68LKd>t_&rAM&K}@(x{q60TKTOL$ z8kUqgy}PC8u=B3ZwW8@NTyt8V-+p6tJul`#PTuz9+rKWhpK*TrZ_~|lr<(g_yiIH` zeq*%I%RZ{MsOazMBa6S=-FKLyv#97W6EhJvoW%=vNJQnSxg2zAZcNcawY=?h+;k#F%}V~^F1#YW=&budG)vGW?7T_ zg0;sVAjbeOl7TV6$RM9Np^!yEMbb)f%Dtr-vK#*F-x6kj&(Zg{VvWFx7ri=4OHy*Z zHqKt39{asA=B4NCb+V?*ALZ@X;d#0B$jevO_v9tc?dOxMo9KQ;NRdxD@3zM3xPNyn zSEmUc22Dpk=M@gSQ4Gv!*-Ha{5t^2yf**< literal 0 HcmV?d00001 diff --git a/tests/san/dns_in_cn.json b/tests/san/dns_in_cn.json new file mode 100644 index 00000000..ec888162 --- /dev/null +++ b/tests/san/dns_in_cn.json @@ -0,0 +1,7 @@ +{ + "CN": "example.com", + "key": { + "algo": "rsa", + "size": 2048 + } +} diff --git a/tests/san/dns_in_san.der b/tests/san/dns_in_san.der new file mode 100644 index 0000000000000000000000000000000000000000..a76bf63e06681e279a8e3aae523994e9e16a5b75 GIT binary patch literal 800 zcmXqLVwN#zVq#gq%*4pV#F4d#AN85K^Mn-N{1_L2O0RuiZ=1>-9 zVfKoO3ccj~Tmw0AULykoV?#qjGXq0Y<0vrK%+SEd$iNK9#j2-?Q3=^-Mpg#qCPsb+ zgC<5UrY1&4hD(vtDi=wy#@*OCFW6Htw#;4Jw!O_OMYd(-hi`gU-!eXW*mT=3^4>Jr zM|0#1-oE2~>3r?$>6aIF8p|y!wQ>%fy;%04hmqQiiswu3U7kDdP0&uePYMnTRGET8 z`6fy+J}>;wYQ1Px+v62(dMxrcq7S~=VUF6F; z8z+4Ih1#!Q-$mw5F?qLZb6uC?^_-gv?nK5pc`n=gfG2m>2ctt5GCm$sYv!0dS^jxW z+?!pzt=sR-@0H+4D!!YbTJvm5Y2-oLw0?o!46=5LB!j=iA5b*UA*ngY}O t3;$?KKUs6vd#AN85K^Mn-N{1_KF0F#{1c=1>-9 z9$qcI)QZI1f}B*ndxxR{z485v%sO@2G?ZWN#6r&o3J8J1mOT_m~x(3IxIp`i!9U;4U|RrI*) zE|vYJ0?U6z2Zcyn`e$e1@s%IJD%3nE;_qEsx|&`HHY-6Z%@jat(DLCl6xBbfO{U;m8V&#er$ zjZDu&i#lXiRI;BpA`^~&JG%1o<`<uo~gHSQC& YtFFhdc06zUHf6tJdeK3~8rP2%0HiB01poj5 literal 0 HcmV?d00001 diff --git a/tests/san/dns_wildcard_in_cn.json b/tests/san/dns_wildcard_in_cn.json new file mode 100644 index 00000000..a99d378d --- /dev/null +++ b/tests/san/dns_wildcard_in_cn.json @@ -0,0 +1,7 @@ +{ + "CN": "*.example.com", + "key": { + "algo": "rsa", + "size": 2048 + } +} diff --git a/tests/san/dns_wildcard_in_san.der b/tests/san/dns_wildcard_in_san.der new file mode 100644 index 0000000000000000000000000000000000000000..ef0d08205c73797369ab070cb4c1bda1d57d7929 GIT binary patch literal 802 zcmXqLVwN*#Vq#ms%*4pV#E~&!_Ri0>Q8EU+Y@Awc9&O)w85y}*84QFB1q}Gum_u2Z zh1n}AD)f@`a}DIgd5sJVj13Kq3{1=ojiSI@GebiIBU2+F7ptBoMkQpU8Ce;an;7{S z44N3Zn3@rmqc$0?AS*JqiHUlp}w^YEHMt|ZYyl8tTvv? zsGo5zZB^o`1539ZbyTmk7nN+N=kqD&-xqUfx@_IQmZn0dnCYvsI_KDgGVGab|9sxo z`7e96=~bk1eiGt2V{`6veRAK!tuG|s`V}4E-1_14?_0Sqj1Rt_XPN7GlK+!=kjKi! zjO7A>5?|QVmoZCku97(9WIsben=jB)Ow8)^fm8Q3t=sh6|LAl^;e=C0#?SKpU--Q8 zI>Vczo9!QIJj~mDqp{(wZtHF)W=00a#T5o+;Lw)kXJq`(!otkNy1+md82YlREMf*C zY#iEbjI6Be%#3grlK~G%S{S69$$$Z(n2$w_Ma1^S^Jo?)ui5GR8;*1dU;oFSaOaSL z1W2(Ui=Y926R(zDYDHphK~Ab(eo;Dd+yJ8;7&nXz9ZUCH&w5$9r?Wik)tw`HE&RN# zEC1-cnXttpvgO<>?fFk%oSO4^bC1XQX+PDbSv@VA_bP;W`?YrSnU{E`x$v3o*u3l1 zg}#|vPBv*Uc3fcpXmKp)-|zSX!spf>b=BB7@z>R0?f3H7daplZ9E`BIyt8YbMmhM(mrMDzxg`=@rj>SNxY0_IbZAu~c>D zwJ#nTxn+xQ9=F>US=ti@&ZG$^5k{^pDJ}7aMu@&N03FYWaaL?6!^v*D$PAe-ZC~9soo{JBlJacBt=q=sZ{%>wqwzekDhYOP9^VO`#jxgADG3h z9DC-*3Y%{t!tJwoXD;Pn6Fz&#?aJz(Jer;oN8%-er+KNYHRamSnU?i%cCNvugvS;| z^{<_jr+OJ&T@X<*-zO#_=C0fJ+svBs zK3H)2=lKgu9RwV2q}hJW5OOq964U>=ah}WJ=$eTNKP~=*E9)w3XWSdHyY$2xfqlwn z@0|MPaWn3R#Cf}|3CA>;%H}gMGcqtPjx&fc-~)!ZEI%XTe-;*ICe{T8vLL=Hiqno&1vH#f}G8ih`6ig^s)3(T=_=u=7|oM{?n({I_R9 zPOX{56~e8a`Lf{d?Q1m=8Zz(eyd5`9esmz@&Ww9`ms~9t$Et5NN;R$Sb!FZpt`?#D z@`dKZY0s8z&`X&#VRD$5-A>1%!pH=sxgHVk&e+#~HhN!p^Q-TyX%>;C(;lYuY-V{c z|7JmI^sL1^U0yc>(**6CYgJlec16rM^2A0UVBW;L3x4sG_rGWV_tNF_gEP&I2fL-7 z2~1}z|*myUhKZ$1YmveA#Y;jsEky z)30Zcjd4#KQ21^N-k}5QiS0 zz6s@Nav>+vK3{+9xaN}Cyxjfs{>-`LeabPH&w7nkzw7->gB6xmbw4JrVio-`M#tx!};2cd|?97aC7Lx%FNLm=AoXLOzqL`0Gj74Ps6N`Tn*F656y?Mr}(Cj}k%R3|A z81RD>3$kz+u(h+)GcYhBM+q>tfl4)LGfXh*`-T$?Nf|p7JA;8 zb@7a2$B>KRTo7t;VsnlhCD literal 0 HcmV?d00001 diff --git a/tests/san/ip_in_san.json b/tests/san/ip_in_san.json new file mode 100644 index 00000000..929d23e7 --- /dev/null +++ b/tests/san/ip_in_san.json @@ -0,0 +1,10 @@ +{ + "CN": "xxx.com", + "hosts": [ + "127.0.0.1" + ], + "key": { + "algo": "rsa", + "size": 2048 + } +} From 5dc9ee7b9cc019e4b082a942002dc38939242fd8 Mon Sep 17 00:00:00 2001 From: "alex.berger@nexiot.ch" Date: Thu, 12 Mar 2020 14:14:29 +0100 Subject: [PATCH 2/2] - By default Subject's CN is no longer considered, but still supported for API clients who need to process old certificates. - Added support for more ASN/DER string types, although they are rarely seen out in the wild, this cannot hurt. - Cleaned up IP address parsing - Added tests for IPv6 --- src/name.rs | 36 ++++++++++---- src/san.rs | 97 +++++++++++++++++++++++-------------- tests/san.rs | 62 +++++++++++++++++++++--- tests/san/dns_in_san.der | Bin 800 -> 800 bytes tests/san/ipv6_in_cn.der | Bin 0 -> 768 bytes tests/san/ipv6_in_cn.json | 7 +++ tests/san/ipv6_in_san.der | Bin 0 -> 805 bytes tests/san/ipv6_in_san.json | 10 ++++ 8 files changed, 159 insertions(+), 53 deletions(-) create mode 100644 tests/san/ipv6_in_cn.der create mode 100644 tests/san/ipv6_in_cn.json create mode 100644 tests/san/ipv6_in_san.der create mode 100644 tests/san/ipv6_in_san.json diff --git a/src/name.rs b/src/name.rs index 0076e546..64c55eb2 100644 --- a/src/name.rs +++ b/src/name.rs @@ -132,7 +132,8 @@ pub fn verify_cert_dns_name( let cert = &cert.inner; let dns_name = untrusted::Input::from(dns_name); iterate_names( - cert.subject, + // For backward compatibility we always pass the subject. + Some(cert.subject), cert.subject_alt_name, Err(Error::CertNotValidForName), &|name| { @@ -182,9 +183,18 @@ pub fn check_name_constraints( let mut child = subordinate_certs; loop { - iterate_names(child.subject, child.subject_alt_name, Ok(()), &|name| { - check_presented_id_conforms_to_constraints(name, permitted_subtrees, excluded_subtrees) - })?; + iterate_names( + Some(child.subject), + child.subject_alt_name, + Ok(()), + &|name| { + check_presented_id_conforms_to_constraints( + name, + permitted_subtrees, + excluded_subtrees, + ) + }, + )?; child = match child.ee_or_ca { EndEntityOrCA::CA(child_cert) => child_cert, @@ -384,8 +394,14 @@ pub(crate) enum NameIteration { Stop(Result<(), Error>), } +/// Nowadays, the subject is ignored and only SANs are considered when +/// validating a certificate's "subject". +/// +/// - https://groups.google.com/a/chromium.org/d/msg/security-dev/IGT2fLJrAeo/csf_1Rh1AwAJ +/// - https://bugs.chromium.org/p/chromium/issues/detail?id=308330 +/// - https://bugzilla.mozilla.org/show_bug.cgi?id=1245280 pub(crate) fn iterate_names( - subject: untrusted::Input, subject_alt_name: Option, + subject: Option, subject_alt_name: Option, result_if_never_stopped_early: Result<(), Error>, f: &dyn Fn(GeneralName) -> NameIteration, ) -> Result<(), Error> { match subject_alt_name { @@ -409,10 +425,12 @@ pub(crate) fn iterate_names( }, None => (), } - - match f(GeneralName::DirectoryName(subject)) { - NameIteration::Stop(result) => result, - NameIteration::KeepGoing => result_if_never_stopped_early, + match subject { + Some(subject) => match f(GeneralName::DirectoryName(subject)) { + NameIteration::Stop(result) => result, + NameIteration::KeepGoing => result_if_never_stopped_early, + }, + _ => result_if_never_stopped_early, } } diff --git a/src/san.rs b/src/san.rs index e03277b3..b6443b3e 100644 --- a/src/san.rs +++ b/src/san.rs @@ -18,6 +18,7 @@ pub enum SubjectAlternativeName { } impl SubjectAlternativeName { + /// Binary OID of CommonName (CN) (id-at-commonName). const OID_CN: [u8; 3] = [85, 4, 3]; fn traverse<'a>( @@ -34,8 +35,41 @@ impl SubjectAlternativeName { /// Strings in Rust are unicode (UTF-8), and unicode codepoints are a /// superset of iso-8859-1 characters. This specific conversion is /// actually trivial. - fn latin1_to_string(s: &[u8]) -> String { - s.iter().map(|&c| c as char).collect() + fn latin1_to_string(s: &[u8]) -> String { s.iter().map(|&c| c as char).collect() } + + fn ucs4_to_string(s: &[u8]) -> Result { + if s.len() % 4 == 0 { + let mut tmp = String::with_capacity(s.len() / 4); + for i in (0..s.len()).step_by(4) { + match std::char::from_u32( + (u32::from(s[i]) << 24) + | (u32::from(s[i]) << 16) + | (u32::from(s[i]) << 8) + | u32::from(s[i + 1]), + ) { + Some(c) => tmp.push(c), + _ => return Err(Error::BadDER), + } + } + Ok(tmp) + } else { + Err(Error::BadDER) + } + } + + fn bmp_to_string(s: &[u8]) -> Result { + if s.len() % 2 == 0 { + let mut tmp = String::with_capacity(s.len() / 2); + for i in (0..s.len()).step_by(2) { + match std::char::from_u32((u32::from(s[i]) << 8) | u32::from(s[i + 1])) { + Some(c) => tmp.push(c), + _ => return Err(Error::BadDER), + } + } + Ok(tmp) + } else { + Err(Error::BadDER) + } } fn extract_common_name(der: &untrusted::Input) -> Option { @@ -51,9 +85,9 @@ impl SubjectAlternativeName { // UTF8String Some((12u8, value)) => String::from_utf8(value.clone()).ok(), // UniversalString (UCS-4 32-bit encoded) - // Some((28u8, value)) => unimplemented!(), + Some((28u8, value)) => Self::ucs4_to_string(value).ok(), // BMPString (UCS-2 16-bit encoded) - //Some((30u8, value)) => unimplemented!(), + Some((30u8, value)) => Self::bmp_to_string(value).ok(), // VideotexString resp. TeletexString ISO-8859-1 encoded Some((21u8, value)) => Some(Self::latin1_to_string(value.as_slice())), _ => None, @@ -66,9 +100,8 @@ impl SubjectAlternativeName { fn matches_dns(dns: &str, name: &GeneralName) -> bool { let dns_input = untrusted::Input::from(dns.as_bytes()); match name { - GeneralName::DNSName(d) => { - presented_dns_id_matches_reference_dns_id(d.clone(), dns_input).unwrap_or(false) - } + GeneralName::DNSName(d) => + presented_dns_id_matches_reference_dns_id(d.clone(), dns_input).unwrap_or(false), GeneralName::DirectoryName(d) => { if let Some(x) = Self::extract_common_name(d) { //x == dns @@ -80,7 +113,7 @@ impl SubjectAlternativeName { } else { false } - } + }, _ => false, } } @@ -90,35 +123,19 @@ impl SubjectAlternativeName { GeneralName::IPAddress(d) => match ip { IpAddr::V4(v4) if d.len() == 4 => { let mut reader = untrusted::Reader::new(d.clone()); - let a = reader.read_byte()?; - let b = reader.read_byte()?; - let c = reader.read_byte()?; - let d = reader.read_byte()?; - Ok(Ipv4Addr::from([a, b, c, d]) == *v4) - } + let mut raw_ip_address: [u8; 4] = Default::default(); + raw_ip_address.clone_from_slice(reader.read_bytes(4)?.as_slice_less_safe()); + Ok(Ipv4Addr::from(raw_ip_address) == *v4) + }, IpAddr::V6(v6) if d.len() == 16 => { let mut reader = untrusted::Reader::new(d.clone()); - let a = reader.read_byte()?; - let b = reader.read_byte()?; - let c = reader.read_byte()?; - let d = reader.read_byte()?; - let e = reader.read_byte()?; - let f = reader.read_byte()?; - let g = reader.read_byte()?; - let h = reader.read_byte()?; - let i = reader.read_byte()?; - let j = reader.read_byte()?; - let k = reader.read_byte()?; - let l = reader.read_byte()?; - let m = reader.read_byte()?; - let n = reader.read_byte()?; - let o = reader.read_byte()?; - let p = reader.read_byte()?; - Ok(Ipv6Addr::from([a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]) == *v6) - } + let mut raw_ip_address: [u8; 16] = Default::default(); + raw_ip_address.clone_from_slice(reader.read_bytes(16)?.as_slice_less_safe()); + Ok(Ipv6Addr::from(raw_ip_address) == *v6) + }, _ => Ok(false), }, - GeneralName::DirectoryName(d) => { + GeneralName::DirectoryName(d) => if let Some(x) = Self::extract_common_name(d) { match IpAddr::from_str(x.as_str()) { Ok(a) => Ok(a == *ip), @@ -126,8 +143,7 @@ impl SubjectAlternativeName { } } else { Ok(false) - } - } + }, _ => Ok(false), } } @@ -141,10 +157,12 @@ impl SubjectAlternativeName { } /// Check if this name is the subject of the provided certificate. - pub fn is_subject_of(&self, cert: &super::EndEntityCert) -> Result<(), Error> { + pub fn is_subject_of_legacy( + &self, cert: &super::EndEntityCert, check_cn: bool, + ) -> Result<(), Error> { let crt = &cert.inner; iterate_names( - crt.subject, + if check_cn { Some(crt.subject) } else { None }, crt.subject_alt_name, Err(Error::CertNotValidForName), &|name| match self.matches(cert, &name) { @@ -154,4 +172,9 @@ impl SubjectAlternativeName { }, ) } + + /// Check if this name is the subject of the provided certificate. + pub fn is_subject_of(&self, cert: &super::EndEntityCert) -> Result<(), Error> { + self.is_subject_of_legacy(cert, false) + } } diff --git a/tests/san.rs b/tests/san.rs index 67c76823..07988e99 100644 --- a/tests/san.rs +++ b/tests/san.rs @@ -1,15 +1,31 @@ #[cfg(feature = "std")] mod tests { - use std::net::{IpAddr, Ipv4Addr}; - use webpki::SubjectAlternativeName; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + use webpki::{Error, SubjectAlternativeName}; + + #[test] + fn dns_in_cn_legacy() { + let der = include_bytes!("san/dns_in_cn.der"); + let cert = webpki::EndEntityCert::from(der).unwrap(); + let name = SubjectAlternativeName::Dns("example.com".to_string()); + assert_eq!(name.is_subject_of_legacy(&cert, true), Ok(())); + } #[test] fn dns_in_cn() { let der = include_bytes!("san/dns_in_cn.der"); let cert = webpki::EndEntityCert::from(der).unwrap(); let name = SubjectAlternativeName::Dns("example.com".to_string()); - assert_eq!(name.is_subject_of(&cert), Ok(())); + assert_eq!(name.is_subject_of(&cert), Err(Error::CertNotValidForName)); + } + + #[test] + fn dns_wildcard_in_cn_legacy() { + let der = include_bytes!("san/dns_wildcard_in_cn.der"); + let cert = webpki::EndEntityCert::from(der).unwrap(); + let name = SubjectAlternativeName::Dns("sub.example.com".to_string()); + assert_eq!(name.is_subject_of_legacy(&cert, true), Ok(())); } #[test] @@ -17,7 +33,7 @@ mod tests { let der = include_bytes!("san/dns_wildcard_in_cn.der"); let cert = webpki::EndEntityCert::from(der).unwrap(); let name = SubjectAlternativeName::Dns("sub.example.com".to_string()); - assert_eq!(name.is_subject_of(&cert), Ok(())); + assert_eq!(name.is_subject_of(&cert), Err(Error::CertNotValidForName)); } #[test] @@ -36,19 +52,51 @@ mod tests { assert_eq!(name.is_subject_of(&cert), Ok(())); } + #[test] + fn ip_in_cn_legacy() { + let der = include_bytes!("san/ip_in_cn.der"); + let cert = webpki::EndEntityCert::from(der).unwrap(); + let name = SubjectAlternativeName::Ip(IpAddr::V4(Ipv4Addr::LOCALHOST)); + assert_eq!(name.is_subject_of_legacy(&cert, true), Ok(())); + } + #[test] fn ip_in_cn() { let der = include_bytes!("san/ip_in_cn.der"); let cert = webpki::EndEntityCert::from(der).unwrap(); - let name = SubjectAlternativeName::Ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); - assert_eq!(name.is_subject_of(&cert), Ok(())); + let name = SubjectAlternativeName::Ip(IpAddr::V4(Ipv4Addr::LOCALHOST)); + assert_eq!(name.is_subject_of(&cert), Err(Error::CertNotValidForName)); + } + + #[test] + fn ipv6_in_cn_legacy() { + let der = include_bytes!("san/ipv6_in_cn.der"); + let cert = webpki::EndEntityCert::from(der).unwrap(); + let name = SubjectAlternativeName::Ip(IpAddr::V6(Ipv6Addr::LOCALHOST)); + assert_eq!(name.is_subject_of_legacy(&cert, true), Ok(())); + } + + #[test] + fn ipv6_in_cn() { + let der = include_bytes!("san/ipv6_in_cn.der"); + let cert = webpki::EndEntityCert::from(der).unwrap(); + let name = SubjectAlternativeName::Ip(IpAddr::V6(Ipv6Addr::LOCALHOST)); + assert_eq!(name.is_subject_of(&cert), Err(Error::CertNotValidForName)); } #[test] fn ip_in_san() { let der = include_bytes!("san/ip_in_san.der"); let cert = webpki::EndEntityCert::from(der).unwrap(); - let name = SubjectAlternativeName::Ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + let name = SubjectAlternativeName::Ip(IpAddr::V4(Ipv4Addr::LOCALHOST)); + assert_eq!(name.is_subject_of(&cert), Ok(())); + } + + #[test] + fn ipv6_in_san() { + let der = include_bytes!("san/ipv6_in_san.der"); + let cert = webpki::EndEntityCert::from(der).unwrap(); + let name = SubjectAlternativeName::Ip(IpAddr::V6(Ipv6Addr::LOCALHOST)); assert_eq!(name.is_subject_of(&cert), Ok(())); } } diff --git a/tests/san/dns_in_san.der b/tests/san/dns_in_san.der index a76bf63e06681e279a8e3aae523994e9e16a5b75..474b2e674d05fed44b6d31f150e506f13dd2139c 100644 GIT binary patch delta 604 zcmV-i0;Bz)2A~EPFoFXdFoFUEpaTK{0s;tGMP3saVBCX|7c3SsFgG+eGdEfn4Kgq= zHZd_VGc__ZH<4L3f8XN<3`>)n0pz9UWoWvsw4ud^b zhrHKt(D-{45{|4AYIV-{g;H&KKHt;aYEg@QpM>LHF~&=Bf5w+A&l)Vg+g1~%Q%o@Bh2j^T{2|$rd+^&!y-Ndlk@>Be`D8u24hJqPLfMX zxJa+b;j9JF!C=dZpnc#DgBcljOzQ;P5oYyq*5TtFZ%E6rKh|`6rw)9d{9jTegD;h;5R&X zvVPki1t-l$*vv)Me{y$Vyv{dHB1Y2dF4>a=W)XwJY zYk3o~^0Tj2358O?F0?BmK8%$)`f4hjuR9^QGdn6H0|@jdUb~M6jT~1iqtTU1xH&_F qGXzh&{<<>hn03nSB;L3XQP(T&-)JwoMwvx&{unJ9B8jU=0=eFMQz2yl delta 604 zcmV-i0;Bz)2A~EPFoFXdFoFUEpaTK{0s;tN3HxUf8*wQ%NCRc1IUIhlWaI9fYay_Ab@!0pj6;+fG^D zmL1}p9x(0h4e3MH^~>qdxicQ7bvi>;o1-1!NHQkac;}_w)198|Qn^0zAV8oc0#a2D zks1N#asLH7qN;}Ds75XXAJ|*L=&`_#aKJQke@=;>IT8YafAX+bqd005_4L0gjWlUa znYY{vR?X6H6^F-la63be4@!QM;!?_Ljevfrm{tAO=liZyLVNr(*(8d9U(|h~y=4w}ViyN@)rwY#85lsO>u-TY&nXXcA z^a8@Cn4(xH0-95kj{#8>)h5nI^F0^q3oKdMo7&?lj8}hdlk@>Bf2uzGjd{dW8kE%| z-n>}*`)&EVA6-D*r6i?R+`nU@rY78t6-O(pT)h%*S(vcIDZ;O@mfb?8DT=`(We~HU?qIzpDMLw?5{u+D(~*lcSL#z?EPduY(Eho*BYq0f>CBq qar`SAPFOlh-v;!0mNw=zx7_0r*cnAsjIm^97*$+Ua%8Ro~jDwF(DkD04n*E*Bo zXKvXnn2>b;*wOUnl3DJVksewpx2E~eb7{UeoxO^CruZJ77l$=h*H-tO_^-O&E}=D! zwRuCA)ddGjpYF-`C6w>!J?Z6Qao7^3pX2-XR`cIUQ%=f%o|Jn<<5lDQ=Blzk%4;j< zL?}t}|JeL$@$BwDRxYi||Ic1`z9MN|Xjpk%m*uSKQ~#zT@APC>`0}~_5}0j%sgqM# z*W=9P|Jsuox1RpIP;)`obvaRmoGlKK#U?p&!g{`%S;rT${jKl)alyOIM$U#~*ZD_Z z|Gn8Pd+*}p2K!f=<(Zfn85kGG8N?Xy0Yh1qpONuD3kx$7>jDE=5MPx=%s_;VLz|6} zm6e^D5zb;V-~mYsgOoEFFhCUZv52vVaEHw5S|q$Jbwc)4>2^_mkt3_3yOCo67^%P* zU}W%pcF@l4nxuvCZL#ZK+cMuQ;4$3V*s1;B_DjTzmYD}?Q}uShW z*I#UJ-c7S~3a~j4=zd%Ck!x0{+`bvzD^FhdwAk##A1~?a2|xGl;1XKieX!-_ftI+# zE0a{(u3Xul5;`f*jU&L;Qf8s&jwf=H%>LW{WeQZUyBTs~-<&+|_`3DqqPY$@-Hv8R zb&vU`nyV`9{(NP)MyT3478N~X)+pzOsiD6WUY5HTTob$7XYhT#tg-d#AN85K^Mn-N{1_L2O0RuiZ=1>-9 zVfKoO3ccj~Tmw0AULykoV?!fD0|Ns?%P26{%+S!#%)k`L#j2-?Q3=^-Mpg#qCPsb+ zgC<5UrY1&4hIzqDRZS*e^?YKo=S|}744c0P(k?Y!JaSPnbV6#}M^CNhFR78&CF?(i zU-yboE8&c=efcdnpXbG^H5=u4md0FrD%={f zsXZe(YGR|nhkrZS)_H4)&Gt)rRv&Hl)@S*@zNEbx<-5LQ#OUSC6>utE-u7_~=e9Lx z?)94-+_m|;?6!tSp7UnA%;0#j_@K(;77+uXb^!)dz=#|@z?cU{4Lng2iEcM&TqB46ervErq17yZRWfq0ND2}jQ{`u literal 0 HcmV?d00001 diff --git a/tests/san/ipv6_in_san.json b/tests/san/ipv6_in_san.json new file mode 100644 index 00000000..8fbb655e --- /dev/null +++ b/tests/san/ipv6_in_san.json @@ -0,0 +1,10 @@ +{ + "CN": "xxx.com", + "hosts": [ + "::1" + ], + "key": { + "algo": "rsa", + "size": 2048 + } +}