Repository: kpcyrd/sn0int (language: rust)

Issue:
x509 certificate parser



Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index b1087f6a..cf020d5b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -456,6 +456,16 @@ dependencies = [
  "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "der-parser"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "nom 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-bigint 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rusticata-macros 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "diesel"
 version = "1.3.3"
@@ -1249,6 +1259,28 @@ dependencies = [
  "memchr 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "num-bigint"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "num_cpus"
 version = "1.8.0"
@@ -1836,6 +1868,14 @@ dependencies = [
  "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "rusticata-macros"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "nom 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "rustls"
 version = "0.13.1"
@@ -2079,6 +2119,7 @@ dependencies = [
  "colored 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "crossbeam-channel 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "der-parser 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "diesel 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "diesel_migrations 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2092,6 +2133,7 @@ dependencies = [
  "maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "maxminddb 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "nom 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "opener 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "pledge 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "publicsuffix 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -2114,6 +2156,7 @@ dependencies = [
  "trust-dns-proto 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "unveil 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "x509-parser 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -2887,6 +2930,19 @@ dependencies = [
  "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "x509-parser"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "der-parser 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "nom 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-bigint 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rusticata-macros 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "xattr"
 version = "0.2.2"
@@ -2949,6 +3005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "630391922b1b893692c6334369ff528dcc3a9d8061ccf4c803aa8f83cb13db5e"
 "checksum curl 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)" = "c7c9d851c825e0c033979d4516c9173bc19a78a96eb4d6ae51d4045440eafa16"
 "checksum curl-sys 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)" = "721c204978be2143fab0a84b708c49d79d1f6100b8785610f456043a90708870"
+"checksum der-parser 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9984fa1478c9bcdabce05d1b1cf2ee7cc134e494d830ab1d3f0f86e284a778f7"
 "checksum diesel 1.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "164080ac16a4d1d80a50f0a623e4ddef41cb2779eee85bcc76907d340dfc98cc"
 "checksum diesel_derives 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "03bcaf77491f53e400d5ee3bdd57142ea4e1c47fe9217b3361ff9a76ca0e3d37"
 "checksum diesel_full_text_search 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7ad3168d9d2008c58b8c9fabb79ddc38d1f9d511fa15e0dcbd6b987912b05783"
@@ -3036,6 +3093,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum nix 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d37e713a259ff641624b6cb20e3b12b2952313ba36b6823c0f16e6cfd9e5de17"
 "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
 "checksum nom 4.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9c349f68f25f596b9f44cf0e7c69752a5c633b0550c3ff849518bfba0233774a"
+"checksum num-bigint 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "10b8423ea72ec64751198856a853e07b37087cfc9b53a87ecb19bff67b6d1320"
+"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea"
+"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1"
 "checksum num_cpus 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c51a3322e4bca9d212ad9a158a02abc6934d005490c054a2778df73a70aa0a30"
 "checksum oauth2 2.0.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0b03cbda9babb980e485e84c7230812f99568b23c68b04008dd0b22f4179a57a"
 "checksum opaque-debug 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "51ecbcb821e1bd256d456fe858aaa7f380b63863eab2eb86eee1bd9f33dd6682"
@@ -3100,6 +3160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "bcfe5b13211b4d78e5c2cadfebd7769197d95c639c35a50057eb4c05de811395"
 "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
 "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+"checksum rusticata-macros 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fe8c976e50876cb3423699b190e799ff4c8a1a4f47dc33f4036dd056c077b7bb"
 "checksum rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "942b71057b31981152970d57399c25f72e27a6ee0d207a669d8304cabf44705b"
 "checksum rustyline 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6010155119d53aac4f5b987cb8f6ea913d0d64d9b237da36f8f96a90cb3f5385"
 "checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7"
@@ -3213,5 +3274,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum winreg 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a"
 "checksum winutil 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7daf138b6b14196e3830a588acf1e86966c694d3e8fb026fb105b8b5dca07e6e"
 "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+"checksum x509-parser 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4c5fcb92ead5ba8eee75f81db4eadd83a022998376afa9e55ca5ba33614bc609"
 "checksum xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c"
 "checksum yansi 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d60c3b48c9cdec42fb06b3b84b5b087405e1fa1c644a1af3930e4dfafe93de48"
diff --git a/Cargo.toml b/Cargo.toml
index c271c766..3125a0ec 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -52,6 +52,9 @@ maxminddb = "0.10.0"
 tar = "0.4.17"
 libflate = "0.1.14"
 threadpool = "1.7"
+x509-parser = "0.4.0"
+der-parser = "1.1.0"
+nom = "4.1.1"
 
 [target.'cfg(target_os="linux")'.dependencies]
 caps = "0.3"
diff --git a/src/crt.rs b/src/crt.rs
new file mode 100644
index 00000000..a13c76d1
--- /dev/null
+++ b/src/crt.rs
@@ -0,0 +1,297 @@
+use errors::*;
+
+use x509_parser;
+use der_parser::oid::Oid;
+use std::collections::HashSet;
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+use nom::be_u8;
+
+
+#[derive(Debug, PartialEq)]
+pub enum AlternativeName {
+    DnsName(String),
+    IpAddr(IpAddr),
+}
+
+named!(san_extension<&[u8], Vec<AlternativeName>>, do_parse!(
+    _tag:   tag!(b"\x30")   >>
+    len:    be_u8           >>
+    values: take!(len)      >>
+    ({
+        let mut bytes = values;
+        let mut values = Vec::new();
+        while !bytes.is_empty() {
+            let (rem, v) = san_value(bytes)?;
+            match v {
+                Ok(v) => values.push(v),
+                Err(e) => warn!("Unknown field in SAN extension: {}", e),
+            }
+            bytes = rem;
+        }
+        values
+    })
+));
+
+named!(san_value<&[u8], Result<AlternativeName>>,
+    switch!(be_u8,
+        0x82    => call!(san_value_dns) |
+        0x87    => call!(san_value_ipaddr) |
+        k       => call!(san_value_unknown, k)
+    )
+);
+
+named!(san_value_dns<&[u8], Result<AlternativeName>>, do_parse!(
+    len:    be_u8           >>
+    value:  take!(len)      >>
+    ({
+        String::from_utf8(value.to_vec())
+            .map(|v| AlternativeName::DnsName(v))
+            .map_err(Error::from)
+    })
+));
+
+named!(san_value_ipaddr<&[u8], Result<AlternativeName>>, do_parse!(
+    len:    be_u8           >>
+    v:      take!(len)      >>
+    ({
+        match len {
+            4  => Ok(AlternativeName::IpAddr(Ipv4Addr::from([
+                v[0], v[1], v[2], v[3],
+            ]).into())),
+            16 => Ok(AlternativeName::IpAddr(Ipv6Addr::from([
+                v[0],  v[1],  v[2],  v[3],
+                v[4],  v[5],  v[6],  v[7],
+                v[8],  v[9],  v[10], v[11],
+                v[12], v[13], v[14], v[15],
+            ]).into())),
+            _ => Err(format_err!("Invalid ipaddr")),
+        }
+    })
+));
+
+named_args!(san_value_unknown(key: u8)<&[u8], Result<AlternativeName>>, do_parse!(
+    len:    be_u8           >>
+    v:      take!(len)      >>
+    ({
+        Err(format_err!("Unexpected type {:?} => {:?}", key, v))
+    })
+));
+
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+pub struct Certificate {
+    pub valid_names: Vec<String>,
+    pub valid_ipaddrs: Vec<IpAddr>
+}
+
+impl Certificate {
+    pub fn parse_pem(crt: &str) -> Result<Certificate> {
+        let pem = match x509_parser::pem::pem_to_der(crt.as_bytes()) {
+            Ok((remaining, pem)) => {
+                if !remaining.is_empty() {
+                    bail!("input cert has trailing garbage");
+                }
+                if pem.label != "CERTIFICATE" {
+                    bail!("input is not a certificate");
+                }
+                pem
+            },
+            Err(_) => bail!("Failed to parse pem"),
+        };
+        Certificate::from_bytes(&pem.contents)
+    }
+
+    pub fn from_bytes(crt: &[u8]) -> Result<Certificate> {
+        let crt = match x509_parser::parse_x509_der(&crt) {
+            Ok((remaining, der)) => {
+                if !remaining.is_empty() {
+                    bail!("input cert has trailing garbage");
+                }
+                if der.tbs_certificate.version != 2 {
+                    bail!("unexpected certificate version");
+                }
+                der
+            },
+            Err(_) => bail!("Failed to parse der"),
+        };
+
+        let mut valid_names = HashSet::new();
+        let mut valid_ipaddrs = HashSet::new();
+
+        for x in crt.tbs_certificate.subject.rdn_seq {
+            for y in x.set {
+                // CommonName
+                if y.attr_type != Oid::from(&[2, 5, 4, 3]) {
+                    continue;
+                }
+
+                if let Ok(x) = y.attr_value.content.as_slice() {
+                    let value = String::from_utf8(x.to_vec())?;
+                    info!("Found CN in Subject: {:?}", value);
+                    valid_names.insert(value);
+                }
+            }
+        }
+
+        for x in crt.tbs_certificate.extensions {
+            if x.oid != Oid::from(&[2, 5, 29, 17]) {
+                continue;
+            }
+
+            debug!("Found san extension: {:?}", x.value);
+            let values = match san_extension(x.value) {
+                Ok((remaining, values)) => {
+                    if !remaining.is_empty() {
+                        bail!("san extension has trailing garbage");
+                    }
+                    values
+                },
+                Err(_) => bail!("Failed to parse san extension"),
+            };
+
+            for v in values {
+                match v {
+                    AlternativeName::DnsName(v) => valid_names.insert(v),
+                    AlternativeName::IpAddr(v) => valid_ipaddrs.insert(v),
+                };
+            }
+        }
+
+        let valid_names = valid_names.into_iter().collect();
+        let valid_ipaddrs = valid_ipaddrs.into_iter().collect();
+        Ok(Certificate {
+            valid_names,
+            valid_ipaddrs,
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_pem_github() {
+        let mut x = Certificate::parse_pem(r#"-----BEGIN CERTIFICATE-----
+MIIHQjCCBiqgAwIBAgIQCgYwQn9bvO1pVzllk7ZFHzANBgkqhkiG9w0BAQsFADB1
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk
+IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE4MDUwODAwMDAwMFoXDTIwMDYwMzEy
+MDAwMFowgccxHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB
+BAGCNzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMRAwDgYDVQQF
+Ewc1MTU3NTUwMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQG
+A1UEBxMNU2FuIEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRMwEQYD
+VQQDEwpnaXRodWIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+xjyq8jyXDDrBTyitcnB90865tWBzpHSbindG/XqYQkzFMBlXmqkzC+FdTRBYyneZ
+w5Pz+XWQvL+74JW6LsWNc2EF0xCEqLOJuC9zjPAqbr7uroNLghGxYf13YdqbG5oj
+/4x+ogEG3dF/U5YIwVr658DKyESMV6eoYV9mDVfTuJastkqcwero+5ZAKfYVMLUE
+sMwFtoTDJFmVf6JlkOWwsxp1WcQ/MRQK1cyqOoUFUgYylgdh3yeCDPeF22Ax8AlQ
+xbcaI+GwfQL1FB7Jy+h+KjME9lE/UpgV6Qt2R1xNSmvFCBWu+NFX6epwFP/JRbkM
+fLz0beYFUvmMgLtwVpEPSwIDAQABo4IDeTCCA3UwHwYDVR0jBBgwFoAUPdNQpdag
+re7zSmAKZdMh1Pj41g8wHQYDVR0OBBYEFMnCU2FmnV+rJfQmzQ84mqhJ6kipMCUG
+A1UdEQQeMByCCmdpdGh1Yi5jb22CDnd3dy5naXRodWIuY29tMA4GA1UdDwEB/wQE
+AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdQYDVR0fBG4wbDA0
+oDKgMIYuaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItZXYtc2VydmVyLWcy
+LmNybDA0oDKgMIYuaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItZXYtc2Vy
+dmVyLWcyLmNybDBLBgNVHSAERDBCMDcGCWCGSAGG/WwCATAqMCgGCCsGAQUFBwIB
+FhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAcGBWeBDAEBMIGIBggrBgEF
+BQcBAQR8MHowJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBS
+BggrBgEFBQcwAoZGaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0
+U0hBMkV4dGVuZGVkVmFsaWRhdGlvblNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAA
+MIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgCkuQmQtBhYFIe7E6LMZ3AKPDWY
+BPkb37jjd80OyA3cEAAAAWNBYm0KAAAEAwBHMEUCIQDRZp38cTWsWH2GdBpe/uPT
+Wnsu/m4BEC2+dIcvSykZYgIgCP5gGv6yzaazxBK2NwGdmmyuEFNSg2pARbMJlUFg
+U5UAdgBWFAaaL9fC7NP14b1Esj7HRna5vJkRXMDvlJhV1onQ3QAAAWNBYm0tAAAE
+AwBHMEUCIQCi7omUvYLm0b2LobtEeRAYnlIo7n6JxbYdrtYdmPUWJQIgVgw1AZ51
+vK9ENinBg22FPxb82TvNDO05T17hxXRC2IYAdgC72d+8H4pxtZOUI5eqkntHOFeV
+CqtS6BqQlmQ2jh7RhQAAAWNBYm3fAAAEAwBHMEUCIQChzdTKUU2N+XcqcK0OJYrN
+8EYynloVxho4yPk6Dq3EPgIgdNH5u8rC3UcslQV4B9o0a0w204omDREGKTVuEpxG
+eOQwDQYJKoZIhvcNAQELBQADggEBAHAPWpanWOW/ip2oJ5grAH8mqQfaunuCVE+v
+ac+88lkDK/LVdFgl2B6kIHZiYClzKtfczG93hWvKbST4NRNHP9LiaQqdNC17e5vN
+HnXVUGw+yxyjMLGqkgepOnZ2Rb14kcTOGp4i5AuJuuaMwXmCo7jUwPwfLe1NUlVB
+Kqg6LK0Hcq4K0sZnxE8HFxiZ92WpV2AVWjRMEc/2z2shNoDvxvFUYyY1Oe67xINk
+myQKc+ygSBZzyLnXSFVWmHr3u5dcaaQGGAR42v6Ydr4iL38Hd4dOiBma+FXsXBIq
+WUjbST4VXmdaol7uzFMojA4zkxQDZAvF5XgJlAFadfySna/teik=
+-----END CERTIFICATE-----
+"#).expect("Failed to parse cert");
+        x.valid_names.sort();
+        assert_eq!(x, Certificate {
+            valid_names: vec!["github.com".into(), "www.github.com".into()],
+            valid_ipaddrs: vec![],
+        });
+    }
+
+    #[test]
+    fn test_parse_pem_1_1_1_1() {
+        let mut x = Certificate::parse_pem(r#"-----BEGIN CERTIFICATE-----
+MIID9DCCA3qgAwIBAgIQBWzetBRl/ycHFsBukRYuGTAKBggqhkjOPQQDAjBMMQsw
+CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSYwJAYDVQQDEx1EaWdp
+Q2VydCBFQ0MgU2VjdXJlIFNlcnZlciBDQTAeFw0xODAzMzAwMDAwMDBaFw0yMDAz
+MjUxMjAwMDBaMGwxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMN
+U2FuIEZyYW5jaXNjbzEZMBcGA1UEChMQQ2xvdWRmbGFyZSwgSW5jLjEdMBsGA1UE
+AwwUKi5jbG91ZGZsYXJlLWRucy5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AASyRQsxrFBjziHmfDQjGsXBU0WWl3oxh7vg6h2V9f8lBMp18PY/td9R6VvJPa20
+AwVzIJI+dL6OSxviaIZEbmK7o4ICHDCCAhgwHwYDVR0jBBgwFoAUo53mH/naOU/A
+buiRy5Wl2jHiCp8wHQYDVR0OBBYEFN+XTeVDs7BBp0LykM+Jf64SV4ThMGMGA1Ud
+EQRcMFqCFCouY2xvdWRmbGFyZS1kbnMuY29thwQBAQEBhwQBAAABghJjbG91ZGZs
+YXJlLWRucy5jb22HECYGRwBHAAAAAAAAAAAAERGHECYGRwBHAAAAAAAAAAAAEAEw
+DgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBp
+BgNVHR8EYjBgMC6gLKAqhihodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1l
+Y2MtZzEuY3JsMC6gLKAqhihodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc3NjYS1l
+Y2MtZzEuY3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAEBMCowKAYIKwYBBQUHAgEW
+HGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQICMHsGCCsGAQUF
+BwEBBG8wbTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEUG
+CCsGAQUFBzAChjlodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRF
+Q0NTZWN1cmVTZXJ2ZXJDQS5jcnQwDAYDVR0TAQH/BAIwADAKBggqhkjOPQQDAgNo
+ADBlAjEAjoyy2Ogh1i1/Kh9+psMc1OChlQIvQF6AkojZS8yliar6m8q5nqC3qe0h
+HR0fExwLAjAueWRnHX4QJ9loqMhsPk3NB0Cs0mStsNDNG6/DpCYw7XmjoG3y1LS7
+ZkZZmqNn2Q8=
+-----END CERTIFICATE-----
+"#).expect("Failed to parse cert");
+        x.valid_names.sort();
+        x.valid_ipaddrs.sort();
+        assert_eq!(x, Certificate {
+            valid_names: vec![
+                "*.cloudflare-dns.com".into(),
+                "cloudflare-dns.com".into(),
+            ],
+            valid_ipaddrs: vec![
+                "1.0.0.1".parse().unwrap(),
+                "1.1.1.1".parse().unwrap(),
+                "2606:4700:4700::1001".parse().unwrap(),
+                "2606:4700:4700::1111".parse().unwrap(),
+            ],
+        });
+    }
+
+    #[test]
+    fn test_san_extension() {
+        let (rem, ext) = san_extension(&[48, 28,
+                130, 10, 103, 105, 116, 104, 117, 98, 46, 99, 111, 109,
+                130, 14, 119, 119, 119, 46, 103, 105, 116, 104, 117, 98, 46, 99, 111, 109
+            ])
+            .expect("Failed to parse extension");
+        assert!(rem.is_empty());
+        assert_eq!(ext, vec![
+            AlternativeName::DnsName(String::from("github.com")),
+            AlternativeName::DnsName(String::from("www.github.com")),
+        ]);
+    }
+
+    #[test]
+    fn test_san_value_dns() {
+        let (rem, v) = san_value(&[130, 10, 103, 105, 116, 104, 117, 98, 46, 99, 111, 109])
+            .expect("Failed to parse san value");
+        let v = v.expect("Extension contains invalid data");
+        assert!(rem.is_empty());
+        assert_eq!(v, AlternativeName::DnsName(String::from("github.com")));
+    }
+
+    #[test]
+    fn test_san_value_ipaddr() {
+        let (rem, v) = san_value(&[135, 4, 1, 1, 1, 1])
+            .expect("Failed to parse san value");
+        let v = v.expect("Extension contains invalid data");
+        assert!(rem.is_empty());
+        assert_eq!(v, AlternativeName::IpAddr("1.1.1.1".parse().unwrap()));
+    }
+}
diff --git a/src/engine/ctx.rs b/src/engine/ctx.rs
index 99c4d49e..733e0e6b 100644
--- a/src/engine/ctx.rs
+++ b/src/engine/ctx.rs
@@ -219,6 +219,7 @@ fn ctx<'a>(env: Environment) -> (hlua::Lua<'a>, Arc<LuaState>) {
     runtime::url_join(&mut lua, state.clone());
     runtime::url_parse(&mut lua, state.clone());
     runtime::utf8_decode(&mut lua, state.clone());
+    runtime::x509_parse_pem(&mut lua, state.clone());
 
     debug!("Created lua context");
 
diff --git a/src/lib.rs b/src/lib.rs
index 3e03657e..ad4e246d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -35,6 +35,8 @@ extern crate maxminddb;
 extern crate tar;
 extern crate libflate;
 extern crate threadpool;
+extern crate x509_parser;
+extern crate der_parser;
 extern crate serde;
 extern crate serde_json;
 extern crate serde_urlencoded;
@@ -45,6 +47,7 @@ extern crate serde_urlencoded;
 #[macro_use] extern crate diesel;
 #[macro_use] extern crate diesel_migrations;
 #[macro_use] extern crate crossbeam_channel as channel;
+#[macro_use] extern crate nom;
 
 pub mod api;
 pub mod archive;
@@ -53,6 +56,7 @@ pub mod auth;
 pub mod cmd;
 pub mod complete;
 pub mod config;
+pub mod crt;
 pub mod db;
 pub mod errors;
 pub mod engine;
diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs
index 57369ee4..fa748642 100644
--- a/src/runtime/mod.rs
+++ b/src/runtime/mod.rs
@@ -20,3 +20,4 @@ import_fns!(regex);
 import_fns!(sleep);
 import_fns!(url);
 import_fns!(utf8);
+import_fns!(x509);
diff --git a/src/runtime/x509.rs b/src/runtime/x509.rs
new file mode 100644
index 00000000..417f91e2
--- /dev/null
+++ b/src/runtime/x509.rs
@@ -0,0 +1,172 @@
+use errors::*;
+
+use crt::Certificate;
+use engine::ctx::State;
+use json::LuaJsonValue;
+use hlua::{self, AnyLuaValue};
+use serde_json;
+use std::sync::Arc;
+
+
+pub fn x509_parse_pem(lua: &mut hlua::Lua, state: Arc<State>) {
+    lua.set("x509_parse_pem", hlua::function1(move |cert: String| -> Result<AnyLuaValue> {
+        let cert = Certificate::parse_pem(&cert)
+            .map_err(|e| state.set_error(e))?;
+
+        let cert = serde_json::to_value(cert)
+            .map_err(|e| state.set_error(e.into()))?;
+
+        Ok(LuaJsonValue::from(cert).into())
+    }))
+}
+
+#[cfg(test)]
+mod tests {
+    use engine::ctx::Script;
+
+    #[test]
+    fn verify_x509_parse_pem_github() {
+        let script = Script::load_unchecked(r#"
+        function run()
+            x = x509_parse_pem([[-----BEGIN CERTIFICATE-----
+MIIHQjCCBiqgAwIBAgIQCgYwQn9bvO1pVzllk7ZFHzANBgkqhkiG9w0BAQsFADB1
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk
+IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE4MDUwODAwMDAwMFoXDTIwMDYwMzEy
+MDAwMFowgccxHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB
+BAGCNzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMRAwDgYDVQQF
+Ewc1MTU3NTUwMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQG
+A1UEBxMNU2FuIEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRMwEQYD
+VQQDEwpnaXRodWIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+xjyq8jyXDDrBTyitcnB90865tWBzpHSbindG/XqYQkzFMBlXmqkzC+FdTRBYyneZ
+w5Pz+XWQvL+74JW6LsWNc2EF0xCEqLOJuC9zjPAqbr7uroNLghGxYf13YdqbG5oj
+/4x+ogEG3dF/U5YIwVr658DKyESMV6eoYV9mDVfTuJastkqcwero+5ZAKfYVMLUE
+sMwFtoTDJFmVf6JlkOWwsxp1WcQ/MRQK1cyqOoUFUgYylgdh3yeCDPeF22Ax8AlQ
+xbcaI+GwfQL1FB7Jy+h+KjME9lE/UpgV6Qt2R1xNSmvFCBWu+NFX6epwFP/JRbkM
+fLz0beYFUvmMgLtwVpEPSwIDAQABo4IDeTCCA3UwHwYDVR0jBBgwFoAUPdNQpdag
+re7zSmAKZdMh1Pj41g8wHQYDVR0OBBYEFMnCU2FmnV+rJfQmzQ84mqhJ6kipMCUG
+A1UdEQQeMByCCmdpdGh1Yi5jb22CDnd3dy5naXRodWIuY29tMA4GA1UdDwEB/wQE
+AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdQYDVR0fBG4wbDA0
+oDKgMIYuaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItZXYtc2VydmVyLWcy
+LmNybDA0oDKgMIYuaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItZXYtc2Vy
+dmVyLWcyLmNybDBLBgNVHSAERDBCMDcGCWCGSAGG/WwCATAqMCgGCCsGAQUFBwIB
+FhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAcGBWeBDAEBMIGIBggrBgEF
+BQcBAQR8MHowJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBS
+BggrBgEFBQcwAoZGaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0
+U0hBMkV4dGVuZGVkVmFsaWRhdGlvblNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAA
+MIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgCkuQmQtBhYFIe7E6LMZ3AKPDWY
+BPkb37jjd80OyA3cEAAAAWNBYm0KAAAEAwBHMEUCIQDRZp38cTWsWH2GdBpe/uPT
+Wnsu/m4BEC2+dIcvSykZYgIgCP5gGv6yzaazxBK2NwGdmmyuEFNSg2pARbMJlUFg
+U5UAdgBWFAaaL9fC7NP14b1Esj7HRna5vJkRXMDvlJhV1onQ3QAAAWNBYm0tAAAE
+AwBHMEUCIQCi7omUvYLm0b2LobtEeRAYnlIo7n6JxbYdrtYdmPUWJQIgVgw1AZ51
+vK9ENinBg22FPxb82TvNDO05T17hxXRC2IYAdgC72d+8H4pxtZOUI5eqkntHOFeV
+CqtS6BqQlmQ2jh7RhQAAAWNBYm3fAAAEAwBHMEUCIQChzdTKUU2N+XcqcK0OJYrN
+8EYynloVxho4yPk6Dq3EPgIgdNH5u8rC3UcslQV4B9o0a0w204omDREGKTVuEpxG
+eOQwDQYJKoZIhvcNAQELBQADggEBAHAPWpanWOW/ip2oJ5grAH8mqQfaunuCVE+v
+ac+88lkDK/LVdFgl2B6kIHZiYClzKtfczG93hWvKbST4NRNHP9LiaQqdNC17e5vN
+HnXVUGw+yxyjMLGqkgepOnZ2Rb14kcTOGp4i5AuJuuaMwXmCo7jUwPwfLe1NUlVB
+Kqg6LK0Hcq4K0sZnxE8HFxiZ92WpV2AVWjRMEc/2z2shNoDvxvFUYyY1Oe67xINk
+myQKc+ygSBZzyLnXSFVWmHr3u5dcaaQGGAR42v6Ydr4iL38Hd4dOiBma+FXsXBIq
+WUjbST4VXmdaol7uzFMojA4zkxQDZAvF5XgJlAFadfySna/teik=
+-----END CERTIFICATE-----
+]])
+            print(x)
+
+            vn = {}
+            i = 0
+            while x['valid_names'][i] do
+                vn[x['valid_names'][i]] = true
+                i = i+1
+            end
+
+            if not vn['www.github.com'] then
+                return 'Not valid for www.github.com'
+            end
+
+            if not vn['github.com'] then
+                return 'Not valid for github.com'
+            end
+
+            if i ~= 2 then
+                return 'Unexpected number of names'
+            end
+        end
+        "#).expect("Failed to load script");
+        script.test().expect("Script failed");
+    }
+
+    #[test]
+    fn verify_x509_parse_pem_1_1_1_1() {
+        let script = Script::load_unchecked(r#"
+        function run()
+            x = x509_parse_pem([[-----BEGIN CERTIFICATE-----
+MIID9DCCA3qgAwIBAgIQBWzetBRl/ycHFsBukRYuGTAKBggqhkjOPQQDAjBMMQsw
+CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSYwJAYDVQQDEx1EaWdp
+Q2VydCBFQ0MgU2VjdXJlIFNlcnZlciBDQTAeFw0xODAzMzAwMDAwMDBaFw0yMDAz
+MjUxMjAwMDBaMGwxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMN
+U2FuIEZyYW5jaXNjbzEZMBcGA1UEChMQQ2xvdWRmbGFyZSwgSW5jLjEdMBsGA1UE
+AwwUKi5jbG91ZGZsYXJlLWRucy5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AASyRQsxrFBjziHmfDQjGsXBU0WWl3oxh7vg6h2V9f8lBMp18PY/td9R6VvJPa20
+AwVzIJI+dL6OSxviaIZEbmK7o4ICHDCCAhgwHwYDVR0jBBgwFoAUo53mH/naOU/A
+buiRy5Wl2jHiCp8wHQYDVR0OBBYEFN+XTeVDs7BBp0LykM+Jf64SV4ThMGMGA1Ud
+EQRcMFqCFCouY2xvdWRmbGFyZS1kbnMuY29thwQBAQEBhwQBAAABghJjbG91ZGZs
+YXJlLWRucy5jb22HECYGRwBHAAAAAAAAAAAAERGHECYGRwBHAAAAAAAAAAAAEAEw
+DgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBp
+BgNVHR8EYjBgMC6gLKAqhihodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc3NjYS1l
+Y2MtZzEuY3JsMC6gLKAqhihodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc3NjYS1l
+Y2MtZzEuY3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAEBMCowKAYIKwYBBQUHAgEW
+HGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQICMHsGCCsGAQUF
+BwEBBG8wbTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEUG
+CCsGAQUFBzAChjlodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRF
+Q0NTZWN1cmVTZXJ2ZXJDQS5jcnQwDAYDVR0TAQH/BAIwADAKBggqhkjOPQQDAgNo
+ADBlAjEAjoyy2Ogh1i1/Kh9+psMc1OChlQIvQF6AkojZS8yliar6m8q5nqC3qe0h
+HR0fExwLAjAueWRnHX4QJ9loqMhsPk3NB0Cs0mStsNDNG6/DpCYw7XmjoG3y1LS7
+ZkZZmqNn2Q8=
+-----END CERTIFICATE-----
+]])
+            print(x)
+
+            vn = {}
+            i = 0
+            while x['valid_names'][i] do
+                vn[x['valid_names'][i]] = true
+                i = i+1
+            end
+
+            vi = {}
+            j = 0
+            while x['valid_ipaddrs'][j] do
+                vi[x['valid_ipaddrs'][j]] = true
+                j = j+1
+            end
+
+            if not vn['*.cloudflare-dns.com'] then
+                return 'Not valid for *.cloudflare-dns.com'
+            end
+            if not vn['cloudflare-dns.com'] then
+                return 'Not valid for cloudflare-dns.com'
+            end
+            if i ~= 2 then
+                return 'Unexpected number of names'
+            end
+
+            if not vi['1.0.0.1'] then
+                return 'Not valid for 1.0.0.1'
+            end
+            if not vi['1.1.1.1'] then
+                return 'Not valid for 1.1.1.1'
+            end
+            if not vi['2606:4700:4700::1001'] then
+                return 'Not valid for 2606:4700:4700::1001'
+            end
+            if not vi['2606:4700:4700::1111'] then
+                return 'Not valid for 2606:4700:4700::1111'
+            end
+            if j ~= 4 then
+                return 'Unexpected number of ip addrs'
+            end
+        end
+        "#).expect("Failed to load script");
+        script.test().expect("Script failed");
+    }
+}


Test patch: