, an open-source service detecting SSL man-in-the-middle

February 20, 2017 is an open-source web service for querying an arbitrary site’s SSL certificate. A user can compare the results of this query with the certificate that her browser is reporting to help determine if there is a man in the middle: Infographic

How it works works by embedding a public key directly into the website’s source:

<script src="" integrity="sha256-WgvkBqG9+UolqdFC1BJOPcy961WTzXj7C9I034ndc4k=" crossorigin="anonymous"></script>
const encrypt = new JSEncrypt();
encrypt.setPublicKey(`-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----`);

When the user wants to check a certificate, she generates a JSON payload to send to the server. This payload contains the URL corresponding to the certificate that the user wants to retrieve, and a random 20 character password:

const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
function randomString(length) {
    let result = '';
    for (let i = length; i > 0; --i) result += alphabet[Math.floor(Math.random() * alphabet.length)];
    return result;
let password = randomString(20);
const optionsObj = {
    url: $("#url").val(),
    password: password
const options = JSON.stringify(optionsObj);

All of this is done in the browser, i.e. on the user’s machine. The payload is encrypted using’s public key:

const options = JSON.stringify(optionsObj);
const encryptedEncodedOptions = encrypt.encrypt(options).replace(/\//g, "_").replace(/\+/g, "-");

This payload is then sent to the remote server for processing:

// urlPrefix is set to a CORS-enabled Amazon API Gateway URL
const url = urlPrefix + encryptedEncodedOptions;
$.ajax({url: url,
  // ...
); decrypts the payload using its (secret) private key. It queries the requested URL for certificate information, then uses the user provided password to encrypt the certificate using AES-256. This payload is sent back in the body of the HTTP response to the client.

The client simply decrypts the results with the password it provided to and displays the results:

<script src="" integrity="sha256-u6BamZiW5tCemje2nrteKC2KoLIKX9lKPSpvCkOhamw=" crossorigin="anonymous"></script>

$.ajax({url: url,
    success: function(response){
        const plainbytes = CryptoJS.AES.decrypt(response, password);
        const plaintext = plainbytes.toString(CryptoJS.enc.Utf8);
        const crt = JSON.parse(plaintext);
        // Render the certificate.
    error: function(result) {
        // Report issues to the user.

Server side

You can set up your own server very easily. Here’s a template for a node.js application:

"use strict";

const express = require("express");
const app = express();
const NodeRSA = require("node-rsa");
const sslCertificate = require('get-ssl-certificate');
const AES = require("crypto-js/aes");
const cors = require('cors');

const corsOptions = {
    origin: ''

const rsa = new NodeRSA(`-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----`, {
    encryptionScheme: 'pkcs1'

app.get('/in/:opt', cors(corsOptions), function(req, res){
    try {
        const optionsJsonEncryptedEncoded = req.params.opt.replace(/\_/g, "/").replace(/\-/g, "+");
        const optionsJsonEncoded = rsa.decrypt(optionsJsonEncryptedEncoded, 'base64');
        const optionsJson = new Buffer(optionsJsonEncoded, 'base64').toString();
        const options = JSON.parse(optionsJson);
        const remoteUrl = options.url;
        const password = options.password;
        if(!password || !remoteUrl) {
            throw new Error("Bad input");
                .then(function(certificate) {
                certificate.success = true;
                certificate.message = `Found certificate for ${remoteUrl}`;
                const plaintext = JSON.stringify(certificate);
                const ciphertext = AES.encrypt(plaintext, password).toString();
                const plaintext = JSON.stringify({
                    success: false,
                    message: `Unable to find certificate for ${remoteUrl}`
                const ciphertext = AES.encrypt(plaintext, password).toString();
    } catch (ex) {


You’ll obviously want to copy the source from and change urlPrefix to point to your server’s /in/ route:

const urlPrefix = ""
const url = urlPrefix + encryptedEncodedOptions;

Then serve the html from some route!

Why does this work?

This technique works because the man in the middle cannot modify your query. It is encrypted with’s public key, and only can decrypt it. Since the man in the middle cannot know the password you provided in your query, it cannot return bogus results to you by encrypting a spoofed response.

If the man in the middle knows about, he could modify’s source. If you want to protect against this attack, you’ll need to verify the source out of band. One way to do this is to visit from a known good location and hash the source. You can then hash the source of in the at-risk setting and compare hashes.

Of course, all bets are off if the man in the middle is also a man in your device. You should give up any expectations of privacy in this case.


We’ll wrap this post up with an example where we inspect Google’s certificate with using Firefox 51.0.1:

Example 1

  • Click the lock:

Example 2

  • Click the right arrow to see additional information about the certificate:

Example 3

  • Click “More Information”:

Example 4

  • Click on the security tab, then “View Certificate”:

Example 5

  • Keep this open. Now visit and query

Example 6

Compare the SHA1 Fingerprints of the results! If there’s a mismatch, you’ve probably got a man in the middle.


Please email me (snuckme at lospi dot net) with any bugs!