Faking the obsolete HTML <keygen> element

This page presents some tools for dealing with websites designed for use with the obsolete HTML <keygen> form element. The tools consist of a Javascript bookmarklet that transforms the page to fill in an answer to the form element, plus some offline Python scripts that generate the answer in the correct format.

These tools are early alpha, not very tested, and not streamlined for general use. If you can’t make sense of the explanations on this page, they may not be for you.

Introduction

Before about 2016, forms in HTML used to be able to contain an input element called <keygen>. When a form containing this element was submitted to a web server, the browser would generate an RSA public/private key pair, and upload the public key to the web server as part of the form submission. The private key would be retained in the browser’s local store of cryptographic material.

This system was used by (at least) certification authorities. If you were applying for a certificate, the CA’s website might present you with an application form including a <keygen>, together with further form fields for all the rest of the details. You’d submit the whole form in one go, and once the CA had done all its checking and validation, it would deliver you a certificate matching the public key you’d uploaded. Your browser would receive that certificate and combine it with the private key that it (hopefully) still had in store.

This system was originally designed (I think) for applications in which the browser itself was going to end up using the private key and certificate, i.e. TLS client certificates. But it was often reused for applications which have nothing to do with that, in which case, after the browser had combined the private key with the certificate, you’d then export the result into a file containing both the private key and the certificate, which you could transfer into whatever application ultimately needed to sign things (a web server, or Authenticode, or whatever).

That was an undesirable way of doing things for many reasons:

In 2016 the <keygen> element was deprecated, and later, removed from the HTML standard. Modern browsers have also dropped support for it – for example, it was removed in Firefox 69, released in 2019.

Unfortunately, as of 2021, some CAs still depend on it for (e.g.) Authenticode, and haven’t updated their procedures to allow certificate applications to be made any other way! Such a CA might suggest, for example, that you use an older browser such as IE11 to work around the lack of <keygen> in anything up to date. That means trusting an obsolete browser, out of security support, with a private key of high value.

If you’d rather not run that risk, here’s an alternative.

Overview

The tools here allow you to use a modern browser, and still successfully submit a form containing a <keygen>, by pretending the browser supports it. This is done by running a Javascript bookmarklet after the page has loaded, which finds the <keygen> and replaces it with a simple <input type="hidden"> containing the same response data that a real <keygen> would have sent.

That response data is known as an SPKAC: “Subject Public Key And Challenge”. It’s a blob of encoded data that includes not only the public key, but a signature made by that key on a challenge string embedded in the <keygen> itself. For example, the server might present a web page including something like <keygen name="pubkey" challenge="12345678">, and then the browser would have to submit a signature covering the string “12345678”.

(The purpose of the challenge string is that it can be different every time, so that the web server can be sure that the person submitting the public key really did have access to the matching private key right now, and didn’t prepare the submission in advance and then leave it somewhere an attacker could substitute a different public key. But not every website will care about making sure of this, so you may find that the challenge is just the empty string.)

This means that you can’t generate the correct SPKAC to respond to a <keygen>, without being able to use the private key. So the JS bookmarklet can’t do the whole job, because you wouldn’t want that bookmarklet to have direct access to your real private key. (Not even if you trust me completely – the bookmarklet runs in the context of an untrusted web page which certainly shouldn’t be able to get hold of the private key, even by accident.)

So, instead, you have to run a tool outside your browser which generates the SPKAC. That tool needs to be able to use the private key (one way or another), and also needs to know the challenge string from the <keygen> element.

Multiple SPKAC generation tools are provided, depending on where your private key lives:

So the expected workflow would be something like this:

More details of each generation script

Here’s some more discussion of each SPKAC generation script, and a fully worked example of generating a key and then generating an SPKAC from it. Click to expand whichever one you need to know more about.

OpenSSL: gen_ossl_spkac.py

The OpenSSL library comes with a command-line tool called openssl, which can generate RSA keys. You’ll have to have this installed anyway, because it’s the same tool that gen_ossl_spkac.py will use to generate the signature in the SPKAC.

To generate a new 3072-bit RSA key (for example) and save it unencrypted to a file on disk, run a command like this:

openssl genrsa -out mykey.pem 3072

If you want to encrypt the key as well, add a cipher option. OpenSSL supports a lot of these (try openssl help to see the full list). An example might be:

openssl genrsa -aes-256-cbc -out mykey.pem 3072

The resulting PEM file will be mostly base64-encoded, and start with a line like “-----BEGIN RSA PRIVATE KEY-----”. If you’ve encrypted the key, then immediately after that header you can also expect to see some headers describing the encryption, so that the file might start like this:

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,17BA61F07B6EDEA0FE081901296A715D

This PEM format for private keys is the same one that OpenSSH has historically used for SSH private keys. So an alternative way to generate a compatible key is to use ssh-keygen, although with modern versions you have to use the -m pem option to stop it from writing out OpenSSH’s newer and more SSH-specific file format. For example, you might replace that generation command with:

ssh-keygen -t rsa -b 3072 -m pem -f mykey.pem

(Of course, if you prefer, you could also use any other SSH key generator that can output in PEM format!)

Once you’ve generated your key, all the SPKAC generation tool needs to know is the challenge string from the <keygen>, and where to find the PEM file. So you might run, for example:

./gen_ossl_spkac.py --challenge="12345678" mykey.pem

If the key is encrypted, you’ll be prompted for its passphrase twice (because the script has to run two separate openssl subcommands, and the second one can’t reuse the decrypted key from the first). After that, the SPKAC should be written to your terminal.

PKCS#11: gen_p11_spkac.py

Many hardware cryptography devices provide a driver in the form of a .so shared library that speaks the PKCS#11 API. pkcs11-tool will talk to those, generate keys, and make signatures.

For example, if you have a Yubikey, then installing the Debian (or Ubuntu) package ykcs11 will provide a file called libykcs11.so in a subdirectory of /usr/lib. On a typical x86-64 Linux system the exact location will be /usr/lib/x86_64-linux-gnu/libykcs11.so.

To generate a key using pkcs11-tool, you can run a command such as

pkcs11-tool --module /usr/lib/x86_64-linux-gnu/libykcs11.so --login --login-type so --keypairgen --id 1 --key-type rsa:2048

This will prompt you for the “SO PIN”. (“SO” stands for “Security Officer” and is the PKCS#11 term for “administrator”: typically a device like this has more than one PIN unlocking different levels of access.) On a Yubikey, for example, this is the thing that ykman thinks of as the “Management Key”.

If that command completes successfully, then the hardware token will now hold a public and private key object under the specified id (in this case, “1”). The token won’t let you extract the private key (that’s most of the point!), but further commands with pkcs11-tool will let you extract the public key, or ask the token to sign things for you. And other software can talk to the same library to generate signatures in the context where you’ll eventually want to use the key.

The SPKAC generation tool needs to know the name of the PKCS#11 driver library, and the id of the key object you just generated (the same as you gave to the --id option in the command above). And, of course, the challenge string from the <keygen>:

./gen_p11_spkac.py --challenge="12345678" /usr/lib/x86_64-linux-gnu/libykcs11.so 1

This time, you’ll likely be prompted for a less privileged PIN to log in to the card with, like the “User PIN”. Enter that in order to authorise the script to generate a signature from the key, and your base64-encoded SPKAC will be printed to the terminal.

GnuPG: gen_gnupg_spkac.py

Perhaps you already have a key pair stored in GnuPG and you want to use that.

You can generate an SPKAC from a GnuPG RSA key by providing the key fingerprint (or its shorter 16-digit id) and the challenge string from the <keygen>:

./gen_gnupg_spkac.py --challenge="12345678" FB8E30B448D4C53E3552E86385116503A8DF59A1

If this requires a key passphrase, the GnuPG agent should prompt you out of band for it with a GUI dialog box, in the same way as a normal GPG signing operation would.

If the key you want to use is a subkey of some other key, you can find out its fingerprint using “gpg --list-keys --with-subkey-fingerprint”.

GnuPG is able to store private keys either in normal disk files or on hardware devices. In principle, this tool should work the same way in both cases, because you ask the GnuPG agent to make a signature in the same way regardless. However, you may find that this doesn’t work for a key on a hardware device, because the SPKAC requires a signature on an MD5 hash, and some PGP-compatible hardware devices will only generate signatures on hashes they approve of. For example, the OpenPGP application on a Yubikey will fail in this way, even though the same signature technique is quite happy to sign SHA-1 or SHA-256 hashes. (And even though the same Yubikey can sign MD5 hashes with a key you can use through its PKCS#11 mode!)

Getting the tools

The Javascript bookmarklet is right here. Right-click and “Bookmark Link”: keygen-fake

To get the Python scripts that generate an SPKAC to use as the response data:

git clone https://git.tartarus.org/simon/keygen-fake.git

Limitations

This is not polished software. It has lots of limitations, and it hasn’t had a lot of testing. Among the limitations:

However, in spite of all that, I’ve used it successfully once, so it might work for you too!

Feedback

Send feedback to anakin@pobox.com.

I don’t expect to have a lot of time to polish this code, so if you want it improved, be prepared to help. Patches welcome!