diff --git a/CHANGELOG b/CHANGELOG
index 5cc64dc92..97af1c00a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,8 @@
# Nmap Changelog ($Id$); -*-text-*-
+o Added the ssl-cert.nse script, which retrieves and prints the server
+ SSL certificate. [David]
+
o [Ncat] The --idle-timeout option now exits when *both* stdin and the
socket have been idle for the given time. Previously it would exit
when *either* of them had been idle, meaning that the program would
diff --git a/scripts/script.db b/scripts/script.db
index cb154481f..8d8924aac 100644
--- a/scripts/script.db
+++ b/scripts/script.db
@@ -57,6 +57,7 @@ Entry { filename = "socks-open-proxy.nse", categories = { "default", "discovery"
Entry { filename = "sql-injection.nse", categories = { "intrusive", "vuln", } }
Entry { filename = "ssh-hostkey.nse", categories = { "default", "intrusive", "safe", } }
Entry { filename = "sshv1.nse", categories = { "default", "safe", } }
+Entry { filename = "ssl-cert.nse", categories = { "discovery", "safe", } }
Entry { filename = "sslv2.nse", categories = { "default", "safe", } }
Entry { filename = "telnet-brute.nse", categories = { "auth", "intrusive", } }
Entry { filename = "upnp-info.nse", categories = { "default", "safe", } }
diff --git a/scripts/ssl-cert.nse b/scripts/ssl-cert.nse
new file mode 100644
index 000000000..f489078c1
--- /dev/null
+++ b/scripts/ssl-cert.nse
@@ -0,0 +1,168 @@
+description = [[
+Retrieves a server's SSL certificate. The amount of information printed
+about the certificate depends on the verbosity level. With no extra
+verbosity, the script prints the validity period and the commonName,
+organizationName, stateOrProvinceName, and countryName of the subject.
+
+
+443/tcp open https
+| ssl-cert: Subject: commonName=www.paypal.com/organizationName=PayPal, Inc.\
+/stateOrProvinceName=California/countryName=US
+| Not valid before: 2009-05-28 00:00:00
+|_ Not valid after: 2010-05-01 23:59:59
+
+
+With -v it adds the issuer name and fingerprints.
+
+
+443/tcp open https
+| ssl-cert: Subject: commonName=www.paypal.com/organizationName=PayPal, Inc.\
+/stateOrProvinceName=California/countryName=US
+| Issuer: commonName=VeriSign Class 3 Extended Validation SSL CA\
+/organizationName=VeriSign, Inc./countryName=US
+| Not valid before: 2009-05-28 00:00:00
+| Not valid after: 2010-05-01 23:59:59
+| MD5: c5b8 7ddd ccc7 537f 8861 b476 078d e8fd
+|_ SHA-1: dc5a cb8b 9eb9 b5de 7117 c536 8c15 0e75 ba88 702e
+
+
+With -vv it adds the PEM-encoded contents of the entire
+certificate.
+
+
+443/tcp open https
+| ssl-cert: Subject: commonName=www.paypal.com/organizationName=PayPal, Inc.\
+/stateOrProvinceName=California/countryName=US/serialNumber=3014267\
+/1.3.6.1.4.1.311.60.2.1.3=US/streetAddress=2211 N 1st St\
+/1.3.6.1.4.1.311.60.2.1.2=Delaware/postalCode=95131-2021\
+/localityName=San Jose/organizationalUnitName=Information Systems\
+/2.5.4.15=V1.0, Clause 5.(b)
+| Issuer: commonName=VeriSign Class 3 Extended Validation SSL CA\
+/organizationName=VeriSign, Inc./countryName=US\
+/organizationalUnitName=Terms of use at https://www.verisign.com/rpa (c)06
+| Not valid before: 2009-05-28 00:00:00
+| Not valid after: 2010-05-01 23:59:59
+| MD5: c5b8 7ddd ccc7 537f 8861 b476 078d e8fd
+| SHA-1: dc5a cb8b 9eb9 b5de 7117 c536 8c15 0e75 ba88 702e
+| -----BEGIN CERTIFICATE-----
+| MIIFxzCCBK+gAwIBAgIQX02QuADDB7CVjZdooVge+zANBgkqhkiG9w0BAQUFADCB
+...
+
+]]
+
+---
+-- @output
+-- 443/tcp open https
+-- | ssl-cert: Subject: commonName=www.paypal.com/organizationName=PayPal, Inc.\
+-- /stateOrProvinceName=California/countryName=US
+-- | Not valid before: 2009-05-28 00:00:00
+-- |_ Not valid after: 2010-05-01 23:59:59
+
+author = "David Fifield "
+
+license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
+
+categories = { "safe", "discovery" }
+
+require("nmap")
+require("nsedebug")
+require("stdnse")
+
+local stringify_name
+local date_to_string
+local table_find
+
+local LIKELY_SSL_PORTS = { 443, 465, 989, 990, 992, 993, 994, 995, 587, 8443 }
+
+portrule = function(host, port)
+ return port.version.service_tunnel == "ssl"
+ or table_find(LIKELY_SSL_PORTS, port.number)
+end
+
+action = function(host, port)
+ local s = nmap.new_socket()
+ local status, error = s:connect(host.ip, port.number, "ssl")
+ if not status then
+ if nmap.verbosity() > 0 then
+ return error
+ else
+ return nil
+ end
+ end
+ local cert = s:get_ssl_certificate()
+ s:close()
+
+ local lines = {}
+ lines[#lines + 1] = "Subject: " .. stringify_name(cert.subject)
+
+ if nmap.verbosity() > 0 then
+ lines[#lines + 1] = "Issuer: " .. stringify_name(cert.issuer)
+ end
+
+ lines[#lines + 1] = "Not valid before: " ..
+ date_to_string(cert.validity.notBefore)
+ lines[#lines + 1] = "Not valid after: " ..
+ date_to_string(cert.validity.notAfter)
+
+ lines[#lines + 1] = "MD5: " .. stdnse.tohex(cert:digest("md5"), { separator = " ", group = 4 })
+ lines[#lines + 1] = "SHA-1: " .. stdnse.tohex(cert:digest("sha1"), { separator = " ", group = 4 })
+
+ if nmap.verbosity() > 1 then
+ lines[#lines + 1] = cert.pem
+ end
+
+ return stdnse.strjoin("\n", lines)
+end
+
+-- Find the index of a value in an array.
+function table_find(t, value)
+ local i, v
+ for i, v in ipairs(t) do
+ if v == value then
+ return i
+ end
+ end
+ return nil
+end
+
+-- These are the subject/issuer name fields that will be shown, in this order,
+-- without a high verbosity.
+local NON_VERBOSE_FIELDS = { "commonName", "organizationName",
+ "stateOrProvinceName", "countryName" }
+
+function stringify_name(name)
+ local fields = {}
+ local _, k, v
+ if not name then
+ return nil
+ end
+ for _, k in ipairs(NON_VERBOSE_FIELDS) do
+ v = name[k]
+ if v then
+ fields[#fields + 1] = string.format("%s=%s", k, v)
+ end
+ end
+ if nmap.verbosity() > 1 then
+ for k, v in pairs(name) do
+ -- Don't include a field twice.
+ if not table_find(NON_VERBOSE_FIELDS, k) then
+ if type(k) == "table" then
+ k = stdnse.strjoin(".", k)
+ end
+ fields[#fields + 1] = string.format("%s=%s", k, v)
+ end
+ end
+ end
+ return stdnse.strjoin("/", fields)
+end
+
+function date_to_string(date)
+ if not date then
+ return "MISSING"
+ end
+ if type(date) == "string" then
+ return string.format("Can't parse; string is \"%s\"", date)
+ else
+ return os.date("%Y-%m-%d %H:%M:%S", os.time(date))
+ end
+end