Some SlackBuild scripts for Slackware.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1306 lines
43 KiB

diff --git a/Makefile b/Makefile
index cb11f89..ba4207e 100644
--- a/Makefile
+++ b/Makefile
@@ -97,6 +97,7 @@ SOURCES := \
inspector.c \
io.c \
js.c \
+ proxy-resolver.c \
requests.c \
scheme.c \
status-bar.c \
@@ -114,6 +115,7 @@ HEADERS := \
inspector.h \
io.h \
js.h \
+ proxy-resolver.h \
requests.h \
menu.h \
scheme.h \
diff --git a/README.md b/README.md
index c879306..29fe73e 100644
--- a/README.md
+++ b/README.md
@@ -1421,6 +1421,11 @@ uzbl itself and will be emitted based on what is happening within uzbl-core.
- Sent when a request has been sent to the server.
* `REQUEST_FINISHED <URI>`
- Sent when a request has completed.
+* `TLS_UPDATE <HOST> <STATUS> <CERTIFICATE>`
+ - Sent when the page has started loading; `status` is one of `NO_TLS`,
+ `TLS_TRUSTED`, `TLS_UNKNOWN_CA`, `TLS_BAD_IDENTITY`, `TLS_BAD_CERTIFICATE`
+ or `TLS_ERROR`; `certificate` is the PEM-encoded certificate(s) provided
+ by the remote host, if any.
##### Input
diff --git a/bin/uzbl-browser.in b/bin/uzbl-browser.in
index 3de99b7..6aa819b 100755
--- a/bin/uzbl-browser.in
+++ b/bin/uzbl-browser.in
@@ -24,6 +24,9 @@ export XDG_CACHE_HOME
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
export XDG_CONFIG_HOME
+XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp}"
+export XDG_RUNTIME_DIR
+
die_with_status () {
status="$1"
shift
diff --git a/bin/uzbl-tabbed b/bin/uzbl-tabbed
index fb8254d..e83ee3d 100755
--- a/bin/uzbl-tabbed
+++ b/bin/uzbl-tabbed
@@ -191,6 +191,13 @@ def xdghome(key, default):
# Setup xdg paths.
DATA_DIR = os.path.join(xdghome('DATA', '.local/share/'), 'uzbl/')
+if 'XDG_RUNTIME_DIR' in os.environ.keys() and os.environ['XDG_RUNTIME_DIR']:
+ RUNTIME_DIR = os.environ['XDG_RUNTIME_DIR']
+else:
+ RUNTIME_DIR = '/tmp'
+ # XDG Base Directory specification mandates this warning.
+ sys.stderr.write("%s: warning: XDG_RUNTIME_DIR is not set\n" % _SCRIPTNAME)
+
# Ensure uzbl xdg paths exist
if not os.path.exists(DATA_DIR):
os.makedirs(DATA_DIR)
@@ -656,8 +663,8 @@ def __init__(self):
self.force_socket_dir = False
self.force_fifo_dir = False
- self.fifo_dir = '/tmp' # Path to look for uzbl fifo.
- self.socket_dir = '/tmp' # Path to look for uzbl socket.
+ self.fifo_dir = RUNTIME_DIR # Path to look for uzbl fifo.
+ self.socket_dir = RUNTIME_DIR # Path to look for uzbl socket.
# Create main window
self.window = gtk.Window()
diff --git a/src/events.h b/src/events.h
index aac76e1..627ef93 100644
--- a/src/events.h
+++ b/src/events.h
@@ -56,6 +56,7 @@
call (WEB_PROCESS_STARTED), \
call (TLS_ERROR), \
call (SCRIPT_MESSAGE), \
+ call (TLS_UPDATE), \
call (SHOW_NOTIFICATION), \
call (CLOSE_NOTIFICATION), \
/* Must be last entry. */ \
diff --git a/src/gui.c b/src/gui.c
index 7467965..ef111f5 100644
--- a/src/gui.c
+++ b/src/gui.c
@@ -818,6 +818,8 @@ struct _UzblGui {
request_decision (const gchar *uri, gpointer data);
static void
send_load_status (WebKitLoadStatus status, const gchar *uri);
+static void
+send_tls_status (WebKitWebView *view);
static gboolean
send_load_error (const gchar *uri, GError *err);
@@ -910,6 +912,9 @@ struct _UzblGui {
const gchar *uri = webkit_web_view_get_uri (view);
send_load_status (event, uri);
+
+ if (event == WEBKIT_LOAD_COMMITTED)
+ send_tls_status (view);
}
gboolean
@@ -1179,6 +1184,9 @@ struct _UzblGui {
const gchar *uri = webkit_web_view_get_uri (view);
send_load_status (status, uri);
+
+ if (status == WEBKIT_LOAD_COMMITTED)
+ send_tls_status (view);
}
gboolean
@@ -2234,6 +2242,90 @@ struct _UzblGui {
return FALSE;
}
+void
+send_tls_status(WebKitWebView *view)
+{
+ GString *pem_chain = NULL;
+ gchar *tls_info;
+ const gchar *host = "";
+ gboolean use_tls = FALSE;
+ GTlsCertificate *cert;
+ GTlsCertificateFlags flags;
+
+#ifdef USE_WEBKIT2
+ SoupURI *uri;
+
+ uri = soup_uri_new (webkit_web_view_get_uri (view));
+ host = soup_uri_get_host (uri);
+
+ use_tls = webkit_web_view_get_tls_info (view, &cert, &flags);
+#else
+ WebKitWebFrame *frame;
+ WebKitNetworkResponse *response;
+
+ frame = webkit_web_view_get_main_frame (view);
+
+ if ((response = webkit_web_frame_get_network_response (frame))) {
+ SoupMessage *message;
+
+ if ((message = webkit_network_response_get_message (response))) {
+ host = soup_uri_get_host (soup_message_get_uri (message));
+ use_tls = soup_message_get_https_status (message, &cert, &flags);
+ }
+ }
+#endif
+
+ if (use_tls) {
+
+ while (cert) {
+ char *pem;
+
+ g_object_get (cert, "certificate-pem", &pem,
+ "issuer", &cert, NULL);
+
+ if (pem) {
+ if (!pem_chain)
+ pem_chain = g_string_new (pem);
+ else
+ g_string_append (pem_chain, pem);
+
+ g_free(pem);
+ }
+ }
+
+ if (flags == 0)
+ tls_info = "TLS_TRUSTED";
+ else if (flags & G_TLS_CERTIFICATE_UNKNOWN_CA)
+ tls_info = "TLS_UNKNOWN_CA";
+ else if (flags & G_TLS_CERTIFICATE_BAD_IDENTITY)
+ tls_info = "TLS_BAD_IDENTITY";
+ else if (flags & (G_TLS_CERTIFICATE_NOT_ACTIVATED |
+ G_TLS_CERTIFICATE_EXPIRED |
+ G_TLS_CERTIFICATE_REVOKED |
+ G_TLS_CERTIFICATE_INSECURE))
+ tls_info = "TLS_BAD_CERTIFICATE";
+ else
+ tls_info = "TLS_ERROR";
+ }
+ else {
+ tls_info = "NO_TLS";
+ }
+
+
+ uzbl_events_send(TLS_UPDATE, NULL,
+ TYPE_STR, host,
+ TYPE_STR, tls_info,
+ TYPE_STR, pem_chain ? pem_chain->str : "",
+ NULL);
+
+ if (pem_chain)
+ g_string_free(pem_chain, TRUE);
+
+#ifdef USE_WEBKIT2
+ soup_uri_free (uri);
+#endif
+}
+
#ifdef USE_WEBKIT2
#if WEBKIT_CHECK_VERSION (2, 1, 4)
void
diff --git a/src/proxy-resolver.c b/src/proxy-resolver.c
new file mode 100644
index 0000000..4e065cc
--- /dev/null
+++ b/src/proxy-resolver.c
@@ -0,0 +1,132 @@
+/*
+ * proxy-resolver - Environment-based proxy resolver for libsoup
+ * Copyright (C) 2010,2011 Damien Goutte-Gattat
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "proxy-resolver.h"
+#include <libsoup/soup-session-feature.h>
+#include <libsoup/soup-proxy-uri-resolver.h>
+
+static SoupURI *
+get_proxy_uri (ProxyResolver *self, SoupURI *uri)
+{
+ SoupURI *proxy = NULL;
+
+ if (g_strcmp0 (uri->scheme, "http") == 0)
+ proxy = self->http_proxy;
+ else if (g_strcmp0 (uri->scheme, "https") == 0)
+ proxy = self->https_proxy;
+
+ if (proxy != NULL && self->no_proxy_hosts != NULL) {
+ char **host = self->no_proxy_hosts;
+
+ while (*host != NULL && proxy != NULL) {
+ if (g_str_has_suffix (uri->host, *host))
+ proxy = NULL;
+ host += 1;
+ }
+ }
+
+ return proxy;
+}
+
+static void
+get_proxy_uri_async (SoupProxyURIResolver *resolver,
+ SoupURI *uri,
+ GMainContext *context,
+ GCancellable *cancellable,
+ SoupProxyURIResolverCallback callback,
+ gpointer user_data)
+{
+ ProxyResolver *self;
+
+ (void) context; (void) cancellable;
+
+ self = PROXY_RESOLVER (resolver);
+ callback (resolver, SOUP_STATUS_OK, get_proxy_uri(self, uri), user_data);
+}
+
+static guint
+get_proxy_uri_sync (SoupProxyURIResolver *resolver,
+ SoupURI *uri,
+ GCancellable *cancellable,
+ SoupURI **proxy_uri)
+{
+ ProxyResolver *self;
+
+ (void) cancellable;
+
+ self = PROXY_RESOLVER (resolver);
+ *proxy_uri = get_proxy_uri (self, uri);
+
+ return SOUP_STATUS_OK;
+}
+
+static void
+soup_proxy_uri_resolver_interface_init (SoupProxyURIResolverInterface *iface)
+{
+ iface->get_proxy_uri_async = get_proxy_uri_async;
+ iface->get_proxy_uri_sync = get_proxy_uri_sync;
+}
+
+G_DEFINE_TYPE_WITH_CODE (ProxyResolver, proxy_resolver,
+ G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (SOUP_TYPE_PROXY_URI_RESOLVER,
+ soup_proxy_uri_resolver_interface_init)
+ G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE, NULL))
+
+static void
+proxy_resolver_finalize (GObject *gobject)
+{
+ ProxyResolver *self;
+
+ self = PROXY_RESOLVER (gobject);
+
+ if (self->http_proxy)
+ soup_uri_free (self->http_proxy);
+
+ if (self->https_proxy)
+ soup_uri_free (self->https_proxy);
+
+ if (self->no_proxy_hosts)
+ g_strfreev (self->no_proxy_hosts);
+
+ G_OBJECT_CLASS (proxy_resolver_parent_class)->finalize (gobject);
+}
+
+static void
+proxy_resolver_init (ProxyResolver *self)
+{
+ const char *s;
+
+ if ((s = g_getenv ("http_proxy")) != NULL)
+ self->http_proxy = soup_uri_new (s);
+
+ if ((s = g_getenv ("https_proxy")) != NULL)
+ self->https_proxy = soup_uri_new (s);
+
+ if ((s = g_getenv ("no_proxy")) != NULL)
+ self->no_proxy_hosts = g_strsplit (s, ",", 0);
+}
+
+static void
+proxy_resolver_class_init (ProxyResolverClass *klass)
+{
+ GObjectClass *gobject_class;
+
+ gobject_class = G_OBJECT_CLASS (klass);
+ gobject_class->finalize = proxy_resolver_finalize;
+}
diff --git a/src/proxy-resolver.h b/src/proxy-resolver.h
new file mode 100644
index 0000000..cf2a809
--- /dev/null
+++ b/src/proxy-resolver.h
@@ -0,0 +1,46 @@
+/*
+ * proxy-resolver - Environment-based proxy resolver for libsoup
+ * Copyright (C) 2010,2011 Damien Goutte-Gattat
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ICP20100103_PROXY_RESOLVER_H
+#define ICP20100103_PROXY_RESOLVER_H
+
+#include <glib-object.h>
+#include <libsoup/soup-uri.h>
+
+#define TYPE_PROXY_RESOLVER (proxy_resolver_get_type())
+#define PROXY_RESOLVER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), TYPE_PROXY_RESOLVER, ProxyResolver))
+#define IS_PROXY_RESOLVER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), TYPE_PROXY_RESOLVER))
+#define PROXY_RESOLVER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), TYPE_PROXY_RESOLVER, ProxyResolverClass))
+#define IS_PROXY_RESOLVER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), TYPE_PROXY_RESOLVER))
+#define PROXY_RESOLVER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), TYPE_PROXY_RESOLVER, ProxyResolverClass))
+
+typedef struct {
+ GObject parent;
+ SoupURI *http_proxy;
+ SoupURI *https_proxy;
+ char **no_proxy_hosts;
+} ProxyResolver;
+
+typedef struct {
+ GObjectClass base;
+} ProxyResolverClass;
+
+GType
+proxy_resolver_get_type (void);
+
+#endif /* !ICP20100103_PROXY_RESOLVER_H */
diff --git a/src/soup.c b/src/soup.c
index 6f209d5..d6f39cd 100644
--- a/src/soup.c
+++ b/src/soup.c
@@ -8,6 +8,7 @@
#include "util.h"
#include "uzbl-core.h"
#include "variables.h"
+#include "proxy-resolver.h"
static void
request_queued_cb (SoupSession *session,
@@ -32,6 +33,9 @@
soup_session_add_feature (session,
SOUP_SESSION_FEATURE (uzbl.net.soup_cookie_jar));
+ soup_session_add_feature (session,
+ SOUP_SESSION_FEATURE (uzbl.net.soup_cookie_jar));
+
g_object_connect (G_OBJECT (session),
"signal::request-queued", G_CALLBACK (request_queued_cb), NULL,
"signal::request-started", G_CALLBACK (request_started_cb), NULL,
diff --git a/uzbl/X509.py b/uzbl/X509.py
new file mode 100644
index 0000000..76488ed
--- /dev/null
+++ b/uzbl/X509.py
@@ -0,0 +1,240 @@
+#!/usr/bin/python3
+
+from pyasn1.codec.der import decoder, encoder
+from pyasn1_modules import rfc2459, pem
+from pyasn1.type import univ, namedtype
+
+from time import strptime
+from base64 import encodebytes
+
+
+#
+# Helper functions
+#
+
+def bitstring2bytes(bitstring):
+ """Converts an ASN.1 BitString to a Python bytes array.
+
+ There should be a better way to to that..."""
+
+ buf = []
+ byte = 0
+ i = 7
+ for bit in bitstring:
+ byte |= bit << i
+ if i == 0:
+ buf.append(byte)
+ byte = 0
+ i = 7
+ else:
+ i -= 1
+ return bytes(buf)
+
+
+_oids = {}
+_oidsList = """
+2.5.4.3 CN
+2.5.4.6 C
+2.5.4.7 L
+2.5.4.8 ST
+2.5.4.10 O
+2.5.4.11 OU
+1.2.840.10045.2.1 EC Public Key
+1.2.840.113549.1.9.1 emailAddress
+1.2.840.113549.1.1.1 RSA Encryption
+1.2.840.113549.1.1.2 MD2 with RSA Encryption
+1.2.840.113549.1.1.3 MD4 with RSA Encryption
+1.2.840.113549.1.1.4 MD5 with RSA Encryption
+1.2.840.113549.1.1.5 SHA1 with RSA Encryption
+1.2.840.113549.1.1.11 SHA256 with RSA Encryption
+1.2.840.113549.1.1.12 SHA384 with RSA Encryption
+1.2.840.113549.1.1.13 SHA512 with RSA Encryption
+1.2.840.113549.1.1.14 SHA224 with RSA Encryption
+"""
+
+for l in [l for l in _oidsList.splitlines() if len(l) > 0]:
+ oid, sep, val = l.partition(' ')
+ _oids[univ.ObjectIdentifier(oid)] = val
+
+
+def oid2string(oid):
+ """Converts an ASN.1 Object Identifier to a string representation."""
+
+ return _oids.get(oid, oid._realstr())
+
+
+# Make pyasn1's ObjectIdentifier object use the above function
+# to convert OID to string
+univ.ObjectIdentifier._realstr = univ.ObjectIdentifier.__str__
+univ.ObjectIdentifier.__str__ = oid2string
+
+
+#
+# Extra ASN.1 objects
+#
+
+class SpecRSAPublicKey(univ.Sequence):
+ componentType = namedtype.NamedTypes(
+ namedtype.NamedType('modulus', univ.Integer()),
+ namedtype.NamedType('exponent', univ.Integer())
+ )
+
+
+#
+# X509 objects
+#
+
+class SubjectPublicKeyInfo:
+
+ RSAKey = univ.ObjectIdentifier('1.2.840.113549.1.1.1')
+
+ def __init__(self, spki):
+ self._spki = spki
+ self._key = None
+
+
+ @property
+ def Algorithm(self):
+ return self._spki.getComponentByName('algorithm').getComponentByName('algorithm')
+
+
+ @property
+ def PublicKey(self):
+ if not self._key:
+ algo = self.Algorithm
+ if algo == self.__class__.RSAKey:
+ keyBits = self._spki.getComponentByName('subjectPublicKey')
+ self._key = decoder.decode(bitstring2bytes(keyBits), asn1Spec=SpecRSAPublicKey())[0]
+ return self._key
+
+
+ @property
+ def DER(self):
+ return encoder.encode(self._spki)
+
+
+ def __str__(self):
+ algo = self.Algorithm
+ if algo == self.__class__.RSAKey:
+ size = int(self.PublicKey.getComponentByName('modulus')).bit_length()
+ return "%s-bit RSA key" % size
+ else:
+ return "Unknown key type (%s)" % oid2string(algo)
+
+
+class Name:
+
+ def __init__(self, name):
+ self._attributes = []
+
+ for attribute in name.getComponentByPosition(0):
+ attrObject = attribute.getComponentByPosition(0)
+ attrType = attrObject.getComponentByPosition(0)
+ attrValue = attrObject.getComponentByPosition(1)
+
+ self._attributes.append((attrType, str(attrValue.asOctets()[2:], 'ascii')))
+
+
+ def __str__(self):
+ s = ""
+ for attr in self._attributes:
+ if len(s) > 0:
+ s += ", "
+ s += "%s=%s" % (oid2string(attr[0]), attr[1])
+ return s
+
+
+class Certificate:
+ """Represents a X.509 certificate."""
+
+ def __init__(self, substrate):
+ self._cert, rest = decoder.decode(substrate, asn1Spec=rfc2459.Certificate())
+ self._tbs = self._cert.getComponentByName('tbsCertificate')
+ self._subject = None
+ self._issuer = None
+ self._validAfter = None
+ self._validBefore = None
+ self._signature = None
+ self._spki = None
+
+
+ @staticmethod
+ def readPEM(f):
+ certs = []
+ while f:
+ idx, substrate = pem.readPemBlocksFromFile(f,
+ ('-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----'))
+ if idx == -1:
+ f.close()
+ f = None
+ else:
+ certs.append(Certificate(substrate))
+ return certs
+
+
+ @property
+ def PEM(self):
+ p = '-----BEGIN CERTIFICATE-----\n'
+ p += str(encodebytes(encoder.encode(self._cert)), 'ascii')
+ p += '-----END CERTIFICATE-----'
+ return p
+
+
+ @property
+ def DER(self):
+ return encoder.encode(self._cert)
+
+
+ @property
+ def Subject(self):
+ if not self._subject:
+ self._subject = Name(self._tbs.getComponentByName('subject'))
+ return self._subject
+
+
+ @property
+ def Issuer(self):
+ if not self._issuer:
+ self._issuer = Name(self._tbs.getComponentByName('issuer'))
+ return self._issuer
+
+
+ @property
+ def Serial(self):
+ return int(self._tbs.getComponentByName('serialNumber'))
+
+
+ @property
+ def ValidAfter(self):
+ if not self._validAfter:
+ notBefore = self._tbs.getComponentByName('validity').getComponentByName('notBefore').getComponentByPosition(0)
+ self._validAfter = strptime(str(notBefore), '%y%m%d%H%M%SZ')
+ return self._validAfter
+
+
+ @property
+ def ValidBefore(self):
+ if not self._validBefore:
+ notAfter = self._tbs.getComponentByName('validity').getComponentByName('notAfter').getComponentByPosition(0)
+ self._validBefore = strptime(str(notAfter), '%y%m%d%H%M%SZ')
+ return self._validBefore
+
+
+ @property
+ def SignatureAlgorithm(self):
+ return self._tbs.getComponentByName('signature').getComponentByName('algorithm')
+
+
+ @property
+ def Signature(self):
+ if not self._signature:
+ sig = self._cert.getComponentByName('signature')
+ self._signature = bitstring2bytes(sig)
+ return self._signature
+
+
+ @property
+ def SubjectPublicKeyInfo(self):
+ if not self._spki:
+ self._spki = SubjectPublicKeyInfo(self._tbs.getComponentByName('subjectPublicKeyInfo'))
+ return self._spki
diff --git a/uzbl/event_manager.py b/uzbl/event_manager.py
index 740e43c..1c011d5 100755
--- a/uzbl/event_manager.py
+++ b/uzbl/event_manager.py
@@ -206,7 +206,7 @@ def run(self):
daemonize()
# Update the pid file
- make_pid_file(opts.pid_file)
+ make_pid_file(self.opts.pid_file)
asyncore.loop()
@@ -517,7 +517,7 @@ def main():
else:
opts.pid_file = expandpath(opts.pid_file)
- config = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation)
+ config = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation())
config.read(opts.config)
# Set default log file location
diff --git a/uzbl/plugins/__init__.py b/uzbl/plugins/__init__.py
index 9bf04d3..711a4d2 100644
--- a/uzbl/plugins/__init__.py
+++ b/uzbl/plugins/__init__.py
@@ -6,8 +6,10 @@
import os.path
+user_plugin_dir = os.environ.get("XDG_DATA_HOME", "~/.local/share") + "/uzbl/plugins"
+
plugin_path = os.environ.get("UZBL_PLUGIN_PATH",
- "~/.local/share/uzbl/plugins:/usr/share/uzbl/site-plugins",
+ user_plugin_dir + ":/usr/local/share/uzbl/plugins:/usr/share/uzbl/plugins",
).split(":")
if plugin_path:
__path__ = list(map(os.path.expanduser, plugin_path)) + __path__
diff --git a/uzbl/plugins/tls_update.py b/uzbl/plugins/tls_update.py
new file mode 100644
index 0000000..25982f2
--- /dev/null
+++ b/uzbl/plugins/tls_update.py
@@ -0,0 +1,589 @@
+# This plugin handles the TLS_UPDATE event.
+
+from uzbl.ext import PerInstancePlugin
+from uzbl.X509 import Certificate
+from .config import Config
+
+from binascii import hexlify, unhexlify
+from hashlib import sha256, sha512, sha1
+from time import strftime, strptime, time, mktime
+from io import StringIO
+
+# For Monkeysphere validation
+from socket import socket, AF_INET, SOCK_STREAM
+from os import getenv
+
+# For DANE validation
+from dns.resolver import Resolver, NXDOMAIN, YXDOMAIN, NoAnswer, NoNameservers, Timeout
+from dns import flags as dnsflags
+
+
+class ValidationResult:
+ """The result of a validation attempt by a Validator object.
+
+ A validation attempt may result in one of the following:
+ * Unvalidated: Validation was actually not attempted.
+ * Error: Validation was attempted, but was prevented
+ by an unknown error.
+ * Untrusted: The certificate could not be verified.
+ * Invalid: The certificate is invalid.
+ * Trusted: The certificate was verified valid."""
+
+ Unvalidated, Error, Untrusted, Invalid, Trusted = range(5)
+ StatusLabels = ["unvalidated", "error", "untrusted", "invalid", "trusted"]
+
+ def __init__(self, validator, status, message):
+ self.validator = validator
+ self.status = status
+ self.message = message
+
+
+ def score(self):
+ return self.validator.Scores[self.status]
+
+
+class Validator:
+ """Abstract validator object.
+
+ This class defines the default scores attributed to each possible
+ validation result. These scores could optionally be modified in
+ a subclass to give more or less weight to a particular validator."""
+
+ def __init__(self):
+ self.Scores = {
+ ValidationResult.Unvalidated: 0,
+ ValidationResult.Error: 0,
+ ValidationResult.Untrusted: 0,
+ ValidationResult.Invalid: -1,
+ ValidationResult.Trusted: 1
+ }
+
+
+ def postValidate(self, host, certificate, score, results):
+ pass
+
+
+class PKIXValidator(Validator):
+ """Validates a certificate through the root Certification
+ Authorities (CAs) trusted by the navigator.
+
+ Actual validation is done directly in uzbl by libsoup routines.
+ This validator merely translates the outcome into a suitable
+ ValidationResult object."""
+
+ Name = 'X.509 Public Keys Infrastructure'
+ Code = 'PKI'
+
+ def validate(self, host, status, certificate, chain):
+ if status == 'NO_TLS':
+ r = ValidationResult(self, ValidationResult.Unvalidated, "No PKIX validation was attempted.")
+ elif status == 'TLS_ERROR':
+ r = ValidationResult(self, ValidationResult.Error, "PKIX validation failed due to an unknown error.")
+ elif status == 'TLS_UNKNOWN_CA':
+ r = ValidationResult(self, ValidationResult.Untrusted, "The certificate for host %s could not be verified because it is signed by an unknown authority." % host)
+ elif status == 'TLS_BAD_CERTIFICATE':
+ r = ValidationResult(self, ValidationResult.Invalid, "The certificate for host %s is either not activated yet, revoked or expired." % host)
+ elif status == 'TLS_BAD_IDENTITY':
+ r = ValidationResult(self, ValidationResult.Invalid, "The certificate for host %s does not match the host's identity." % host)
+ elif status == 'TLS_TRUSTED':
+ r = ValidationResult(self, ValidationResult.Trusted, "The certificate for host %s is valid." % host)
+ else:
+ r = ValidationResult(self, ValidationResult.Error, "This should never happen.")
+
+ return r
+
+
+class MonkeysphereValidator(Validator):
+ """Validates a certificate through the Monkeysphere web-of-trust.
+
+ This validator depends on a Monkeysphere validation agent (such as
+ msva-perl) running on the local machine and advertised in the
+ MONKEYSPHERE_VALIDATION_AGENT_SOCKET environment variable."""
+
+ Name = 'Monkeysphere Web-of-trust'
+ Code = 'WOT'
+
+ def __init__(self):
+ super(MonkeysphereValidator, self).__init__()
+ agent = getenv('MONKEYSPHERE_VALIDATION_AGENT_SOCKET')
+ if not agent:
+ self.available = False
+
+ try:
+ i = agent.rindex(':')
+ self.port = int(agent[i+1:])
+ except:
+ self.available = False
+
+ # Check that the agent is indeed present and is truly a
+ # Monkeysphere validation agent.
+ try:
+ sock = socket(AF_INET, SOCK_STREAM, 6)
+ sock.connect(('localhost', self.port))
+ reply = self._request(sock, 'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
+ self.available = reply.find('"available":true') != -1
+ except:
+ self.available = False
+
+
+ def _request(self, sock, request):
+ try:
+ sock.sendall(bytearray(request, 'ascii'))
+ reply = bytearray(512)
+ sock.recv_into(reply)
+ return reply.decode('ascii')
+ finally:
+ sock.close()
+
+
+ def validate(self, host, status, certificate, chain):
+ if not self.available:
+ return ValidationResult(self, ValidationResult.Error, "The Monkeysphere validation agent could not be contacted.")
+
+ body = '{"peer":{"name":"%s","type":"server"},"context":"https",' \
+ '"pkc":{"type":"x509pem","data":"%s"}}' % (host, certificate.PEM.replace('\n', '\\n'))
+ query = 'POST /reviewcert HTTP/1.1\r\n' \
+ 'Host: localhost\r\n' \
+ 'Content-Type: application/json\r\n' \
+ 'Content-Length: %d\r\n' \
+ 'Accept: application/json\r\n' \
+ 'Connection: close\r\n' \
+ '\r\n' \
+ '%s\r\n' % (len(body), body)
+
+ try:
+ sock = socket(AF_INET, SOCK_STREAM, 6)
+ sock.connect(('localhost', self.port))
+ reply = self._request(sock, query)
+ if reply.find('"valid":true') != -1:
+ return ValidationResult(self, ValidationResult.Trusted, "The certificate for host %s was found in the web-of-trust and is deemed valid." % host)
+ else:
+ return ValidationResult(self, ValidationResult.Untrusted, "The certificate for host %s was not found in the web-of-trust or is not trusted." % host)
+ except:
+ return ValidationResult(self, ValidationResult.Error, "Validation through the web-of-trust failed due to an unknown error.")
+ finally:
+ sock.close()
+
+
+# RFC 7218 constants for the DANE validator
+PKIX_TA, PKIX_EE, DANE_TA, DANE_EE = range(4)
+CERT, SPKI = range(2)
+FULL, SHA256, SHA512 = range(3)
+
+class DANEValidator(Validator):
+ """Validates a certificate against DNS records.
+
+ This validator partially implements RFC 6698. It lookups for
+ TLSA records in the DNS for the desired host and, if present,
+ verifies that the provided certificate matches what is
+ specified in said record.
+
+ This validator depends on a trusted and DNSSEC-aware resolver,
+ which should ideally be running on the local machine."""
+
+ Name = 'DNS-based Authentication of Named Entities'
+ Code = 'DNS'
+
+
+ def __init__(self):
+ super(DANEValidator, self).__init__()
+ self.resolver = Resolver()
+ self.resolver.ednsflags = dnsflags.DO
+ self.resolver.flags = dnsflags.AD
+
+
+ def _checkRecord(self, certificate, tlsa):
+ if tlsa.selector == CERT:
+ cert_data = certificate.DER
+ else:
+ cert_data = certificate.SubjectPublicKeyInfo.DER
+
+ if tlsa.mtype == FULL:
+ match = cert_data == tlsa.cert
+ elif tlsa.mtype == SHA256:
+ match = sha256(cert_data).digest() == tlsa.cert
+ else:
+ match = sha512(cert_data).digest() == tlsa.cert
+
+ return match
+
+
+ def validate(self, host, status, certificate, chain):
+ try:
+ rrset = self.resolver.query('_443._tcp.' + host, rdtype='TLSA')
+ except NXDOMAIN:
+ return ValidationResult(self, ValidationResult.Untrusted, "No TLSA records were found for host %s." % host)
+ except NoAnswer:
+ return ValidationResult(self, ValidationResult.Error, "No TLSA records were found for host %s, although suitable names do exist in the DNS zone." % host)
+ except YXDOMAIN:
+ return ValidationResult(self, ValidationResult.Error, "Too much DNAME substitution in the query for TLSA records.")
+ except Timeout:
+ return ValidationResult(self, ValidationResult.Error, "No answers for TLSA records could be obtained in time.")
+ except NoNameservers:
+ return ValidationResult(self, ValidationResult.Error, "No non-broken nameservers are available to lookup for TLSA records.")
+
+ if rrset.response.flags & dnsflags.AD == 0:
+ # Records found, but with insecure or indeterminate DNSSEC status;
+ # Such records are unusable as per RFC 6609 §4.1
+ return ValidationResult(self, ValidationResult.Untrusted, "No secure TLSA records were found for host %s." % host)
+
+ for record in rrset:
+ if record.usage in (PKIX_TA, PKIX_EE) and not status == 'TLS_TRUSTED':
+ # Do not attempt to validate against this record: since the
+ # certificate is not trusted according to PKIX, we already
+ # know not to trust it.
+ continue
+
+ if record.usage in (PKIX_EE, DANE_EE) and self._checkRecord(certificate, record):
+ # Even if there are more TLSA records, a successful
+ # validation against any one of them is enough, per
+ # RFC 6698 §4.1.
+ return ValidationResult(self, ValidationResult.Trusted, "The certificate for host %s matches a published TLSA record." % host)
+ elif record.usage in (PKIX_TA, DANE_TA):
+ # Validate a certificate from the issuer chain.
+ for issuerCert in chain:
+ if self._checkRecord(issuerCert, record):
+ return ValidationResult(self, ValidationResult.Trusted, "The certificate for host %s is signed by a certificate matching a published TLSA record." % host)
+
+ return ValidationResult(self, ValidationResult.Invalid, "The certificate for host %s does not match any of the usable TLSA record." % host)
+
+
+
+class TrackingValidator(Validator):
+
+ Name = 'Host/Certificate Association Tracker'
+ Code = 'TRK'
+
+ def __init__(self, store, tofu=False):
+ super(TrackingValidator, self).__init__()
+ self._history = {}
+ self._file = store
+ self._tofu = tofu
+ self.loadHistory(None)
+
+
+ def addHostCertificate(self, host, certificate):
+ certHash = sha256(certificate.SubjectPublicKeyInfo.DER).digest()
+ certExpire = strftime('%Y%m%d%H%M%S', certificate.ValidBefore)
+ self._history[host] = (certHash, certExpire)
+
+ f = open(self._file, 'a')
+ f.write('%s %s %s\n' % (str(hexlify(certHash), 'ascii'), certExpire, host))
+ f.close()
+
+
+ def loadHistory(self, dummy):
+ self._history.clear()
+ try:
+ f = open(self._file, 'r')
+ except FileNotFoundError:
+ return
+
+ for line in f:
+ try:
+ certHash, certExpire, host = line.split()
+ self._history[host] = (unhexlify(certHash),
+ strptime(certExpire, '%Y%m%d%H%M%S'))
+ except:
+ pass
+ f.close()
+
+
+ def validate(self, host, tls_status, certificate, chain):
+ newHash = sha256(certificate.SubjectPublicKeyInfo.DER).digest()
+
+ try:
+ oldHash, expire = self._history[host]
+ if newHash == oldHash:
+ return ValidationResult(self, ValidationResult.Trusted, "The presented certificate for host %s matches the one previously recorded for that host." % host)
+ elif mktime(expire) < time():
+ # Old certificate has expired, consider the new
+ # certificate as untrusted
+ return ValidationResult(self, ValidationResult.Untrusted, "The certificate previously associated with that host has expired." % host)
+ else:
+ return ValidationResult(self, ValidationResult.Invalid, "The presented certificate for host %s differs from the one previously recorded for that host." % host)
+ except KeyError:
+ return ValidationResult(self, ValidationResult.Untrusted, "No certificate has been recorded previously for host %s." % host)
+
+
+ def postValidate(self, host, certificate, score, results):
+ # Consider adding the certificate to the store
+ # - in default mode: only if the certificate was found trusted
+ # - in TOFU mode: if it was not found invalid
+ if score > 0 or (self._tofu and score > -1):
+ result = [r for r in results if r.validator == self][0]
+ # If this validator found the certificate untrusted (unknown
+ # host), we can add it to the store. But if it found the
+ # certificate invalid (because it differed from a
+ # previously known certificate for that host), then even
+ # in TOFU mode, we add the new certificate only if other
+ # validators trusted it.
+ if result.status == ValidationResult.Untrusted or (result.status == ValidationResult.Invalid and score > 0):
+ self.addHostCertificate(host, certificate)
+
+
+class CertificateQualityValidator(Validator):
+ """Validates a certificate if it meets some quality criteria."""
+
+ Name = 'Certificate Quality Checker'
+ Code = 'CRQ'
+
+ def __init__(self):
+ super(CertificateQualityValidator, self).__init__()
+ # The mere fact that a certificate meets the quality
+ # criteria does not mean that we should trust it.
+ self.Scores[ValidationResult.Trusted] = 0
+
+
+ def validate(self, host, status, certificate, chain):
+ if str(certificate.SignatureAlgorithm) == 'SHA1 with RSA Encryption':
+ return ValidationResult(self, ValidationResult.Invalid, "Certificate uses SHA1 algorithm.")
+
+ spki = certificate.SubjectPublicKeyInfo
+ if str(spki.Algorithm) == 'RSA Encryption':
+ keySize = int(spki.PublicKey.getComponentByName('modulus')).bit_length()
+ if keySize < 1536:
+ return ValidationResult(self, ValidationResult.Invalid, "Certificate's RSA key is too small.")
+
+ return ValidationResult(self, ValidationResult.Trusted, "The certificate is secure enough.")
+
+
+class TlsPlugin(PerInstancePlugin):
+ """Handler for the TLS_UPDATE event.
+
+ This plugin handles TLS_UPDATE event emitted by Uzbl after
+ committing a secure page load. It allows to perform supplementary
+ validation checks beyond those already done by libsoup, and fills
+ the 'tls_status' variable with markup ready to be displayed in
+ the browser's status bar.
+
+ Supplementary validators are disabled by default. To enable them,
+ set the following variables to 'true' in the event manager's
+ configuration file:
+
+ * monkeysphere_enabled: for the Monkeysphere validator;
+ * dane_enabled: for the DNS-based (RFC 6698) validator;
+ * certtracker_enabled: for the tracking validator."""
+
+ CONFIG_SECTION = 'tls_update'
+
+ def __init__(self, uzbl):
+ super(TlsPlugin, self).__init__(uzbl)
+ uzbl.connect('TLS_UPDATE', self.tlsUpdate)
+ self.validators = [PKIXValidator()]
+ self.tmpdir = getenv('XDG_RUNTIME_DIR', '/tmp')
+
+ if self.plugin_config.getboolean('monkeysphere_enabled'):
+ self.validators.append(MonkeysphereValidator())
+
+ if self.plugin_config.getboolean('dane_enabled'):
+ self.validators.append(DANEValidator())
+
+ if self.plugin_config.getboolean('qualitychecker_enabled'):
+ self.validators.append(CertificateQualityValidator())
+
+ if self.plugin_config.getboolean('certtracker_enabled'):
+ store = self.plugin_config.get('certtracker_store', fallback='%s/uzbl/tls_host_certificates' % getenv('XDG_DATA_HOME'))
+ tofu = self.plugin_config.getboolean('certtracker_tofu')
+
+ tracker = TrackingValidator(store, tofu=tofu)
+ self.validators.append(tracker)
+ uzbl.connect('TLS_TRACKER_RELOAD', tracker.loadHistory)
+
+
+ def tlsUpdate(self, args):
+ host, tls_status, tls_cert = [arg[1:-1].replace('\\n', '\n') for arg in args.split(' ', maxsplit=2)]
+
+ if tls_status == 'NO_TLS': # Plaintext connection
+ self.uzbl.send('set tls_status')
+ return
+
+ certs = Certificate.readPEM(StringIO(tls_cert))
+
+ # Run validators.
+ results = []
+ score = 0
+ for validator in self.validators:
+ r = validator.validate(host, tls_status, certs[0], certs[1:])
+ results.append(r)
+ score += r.score()
+
+ for validator in self.validators:
+ validator.postValidate(host, certs[0], score, results)
+
+ if score > 0:
+ # Format validation results in the tls_status variable.
+ label = ""
+ for result in results:
+ if result.status == ValidationResult.Error:
+ label += ' <span weight="bold" foreground="darkgray">%s</span>' % result.validator.Code
+ elif result.status == ValidationResult.Untrusted:
+ label += ' <span weight="bold" foreground="orange">%s</span>' % result.validator.Code
+ elif result.status == ValidationResult.Invalid:
+ label += ' <span weight="bold" foreground="red">%s</span>' % result.validator.Code
+ elif result.status == ValidationResult.Trusted:
+ label += ' <span weight="bold" foreground="darkgreen">%s</span>' % result.validator.Code
+
+ self.uzbl.send('set tls_status %s' % label)
+ else:
+ # Certificate untrusted or invalid, abort the connection
+ # and display an error page.
+ self.showErrorPage(host, certs[0], results)
+
+
+ def showErrorPage(self, host, certificate, results):
+ html = """<html>
+ <head>
+ <title>TLS Error</title>
+ <style text="text/css">
+ table {
+ border-collapse: collapse;
+ border: solid 1px #000000;
+ }
+
+ thead {
+ text-align: center;
+ font-weight: bold;
+ background-color: #000000;
+ color: #eeeeee;
+ }
+
+ th {
+ font-weight: bold;
+ background-color: #000000;
+ color: #eeeeee;
+ }
+
+ td {
+ padding: .2ex .5em .2ex .5em;
+ border: solid 1px #000000;
+ }
+
+ td.error {
+ background-color: #cccccc;
+ }
+
+ td.untrusted {
+ background-color: #ff8800;
+ }
+
+ td.invalid {
+ background-color: #ff0000;
+ }
+
+ td.trusted {
+ background-color: #00ff00;
+ }
+
+ td.score {
+ text-align: center;
+ }
+
+ td.value {
+ font-family: monospace;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>Cannot connect securely to host %s</h1>
+ <h2>Validation status</h2>
+ <table>
+ <thead>
+ <td>Validation method</td>
+ <td>Result</td>
+ <td>Score</td>
+ <td>Comment</td>
+ </thead>
+ <tbody>""" % host
+
+ for result in results:
+ html += """
+ <tr>
+ <td>%s</td>
+ <td class="%s">%s</td>
+ <td class="score">%d</td>
+ <td>%s</td>
+ </tr>""" % (result.validator.Name,
+ ValidationResult.StatusLabels[result.status],
+ ValidationResult.StatusLabels[result.status].capitalize(),
+ result.score(),
+ result.message)
+
+ html += """
+ </tbody>
+ </table>
+ <h2>Certificate provided by remote host</h2>
+ <table>
+ <tbody>
+ <tr>
+ <th scope="rowgroup" colspan="2">Certificate</th>
+ </tr>
+ <tr>
+ <td class="name">Subject</td>
+ <td class="value">%s</td>
+ </tr>
+ <tr>
+ <td class="name">Issuer</td>
+ <td class="value">%s</td>
+ </tr>
+ <tr>
+ <td class="name">Serial number</td>
+ <td class="value">%s</td>
+ </tr>
+ <tr>
+ <td class="name">Valid from</td>
+ <td class="value">%s</td>
+ </tr>
+ <tr>
+ <td class="name">Valid until</td>
+ <td class="value">%s</td>
+ </tr>
+ <tr>
+ <td class="name">Signature algorithm</td>
+ <td class="value">%s</td>
+ </tr>
+ <tr>
+ <td class="name">SHA1 fingerprint</td>
+ <td class="value">%s</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th scope="rowgroup" colspan="2">Public Key</th>
+ </tr>
+ <tr>
+ <td class="name">Key type</td>
+ <td class="value">%s</td>
+ </tr>
+ <tr>
+ <td class="name">SHA256 fingerprint</td>
+ <td class="value">%s</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th scope="rowgroup" colspan="2">PEM-encoded Certificate</th>
+ </tr>
+ <tr>
+ <td colspan="2"><pre>%s</pre></td>
+ </tr>
+ </tbody>
+ </table>
+ </body>
+</html>""" % (certificate.Subject,
+ certificate.Issuer,
+ certificate.Serial,
+ strftime('%c', certificate.ValidAfter),
+ strftime('%c', certificate.ValidBefore),
+ certificate.SignatureAlgorithm,
+ str(hexlify(sha1(certificate.DER).digest()), 'ascii'),
+ certificate.SubjectPublicKeyInfo,
+ str(hexlify(sha256(certificate.SubjectPublicKeyInfo.DER).digest()), 'ascii'),
+ certificate.PEM)
+
+ tmpfile = "%s/uzbl.tls_update.%s.html" % (self.tmpdir, host)
+ f = open(tmpfile, 'w')
+ f.write(html)
+ f.close()
+
+ self.uzbl.send('uri file://%s' % tmpfile)