diff --git a/doc/luasec.md b/doc/luasec.md index c57083e..9ae4dc1 100644 --- a/doc/luasec.md +++ b/doc/luasec.md @@ -47,3 +47,13 @@ Alias for `cert.load`. `ssl.wrap` wraps an existing luasocket socket into a luasec connection object. `cfg` is defined as for `ssl.newcontext`. + +### ssl.checkhostname ### + + valid = ssl.checkhostname(cert, hostname) + +Check if the certificate is valid for the given hostname. Deals with wildcards +and alternative names. + +**NOTE**: It is crucial the hostname is checked to verify the certificate is +not only valid, but belonging to the host connected to. diff --git a/doc/ssl.md b/doc/ssl.md index 3d2bd29..57523ec 100644 --- a/doc/ssl.md +++ b/doc/ssl.md @@ -119,3 +119,10 @@ context associated with this `conn` object. Returns luasec's current `want`, if the connection is dirty (see `conn:dirty`). This can either be `nothing`, `read`, `write` or `x509lookup`. + +### conn:checkhostname ### + + valid = conn:checkhostname(hostname) + +Checks whether the certificate is valid for the given hostname. Helper for +`ssl.checkhostname`, see `ssl.checkhostname` for more information. diff --git a/src/ssl.lua b/src/ssl.lua index afc40a5..3f42de5 100644 --- a/src/ssl.lua +++ b/src/ssl.lua @@ -159,10 +159,57 @@ local function info(ssl, field) return ( (next(info)) and info ) end +-- +-- Verify host name against a common name +-- +local function checkhostname_single(hostname, cn) + if cn:match("^%*%.") then + -- If it's a wildcard domain name, strip the first element of the hostname + -- and the cn, then check neither are empty. + hostname = hostname:match("%.(.+)$") + cn = cn:match("%.(.+)$") + if cn == "" or hostname == "" then return false end + end + return cn == hostname +end + +-- +-- Verify host name against certificate +-- +local function checkhostname(cert, hostname) + local subject, ext + subject = cert:subject() + for i, v in ipairs(subject) do + if v.name == "commonName" then + if checkhostname_single(hostname, v.value) then + return true + end + break + end + end + -- If we got here, the cn doesn't match, check for the dNSName extension + ext = (cert:extensions() or {})["2.5.29.17"] + if not ext or not ext.dNSName then return false end + for i, v in ipairs(ext.dNSName) do + if checkhostname_single(hostname, v) then + return true + end + end + return false +end + +-- +-- Verify host name against peer certificate +-- +local function checkhostname_ssl(ssl, hostname) + return checkhostname(ssl:getpeercertificate(), hostname) +end + -- -- Set method for SSL connections. -- core.setmethod("info", info) +core.setmethod("checkhostname", checkhostname_ssl) -------------------------------------------------------------------------------- -- Export module @@ -174,6 +221,7 @@ local _M = { loadcertificate = x509.load, newcontext = newcontext, wrap = wrap, + checkhostname = checkhostname, } return _M