Memoirs from the old web: The KEYGEN element

The <keygen/> element is probably one of the weirdest elements ever added to HTML:

<form method="POST">
  <keygen
    name="key"
    challenge="...optional challenge string..."
    keytype="RSA" />
  <button type="submit">Submit</button>
</form>

The purpose of the <keygen/> element was to allow a web browser to generate a private/public keypair upon submitting a form, in a way that allowed a web browser to be enrolled in a new client certificate.

Anyone who has setup a website will appreciate that SSL/TLS requires a web server to have an SSL certificate authenticating its identity. However, while TLS requires a server certificate, it also has optional support for client certificates, whereby a client connecting to a TLS server must also authenticate its identity to the server. Just as in the case of server certificates, this is done by presenting a certificate and proving ownership of the corresponding private key.

Client certificates are rarely used on the web today, though the client certificate functionality of TLS is popular in some non-web contexts1. Yet there is—or was—more infrastructure in web browsers to handle client certificates than many realise. This infrastructure, and the <keygen/> element dates back to Netscape 4.0. Possibly, when cryptographic functionality such as SSL was first being added to web browsers, there was substantially more optimism about the viability of client certificates on the web than there is now.

Why are client certificates rarely used? There are likely several reasons, but probably the most obvious one is that the UI for handling them has been truly abysmal. The bare minimum functionality and UI for enrolling a web browser in a new client certificate was added to Netscape 4.0 and seems to have received basically no maintenance or enhancement since.

This unloved feature was finally axed in Firefox 68, released in 2019, which removed support for <keygen/>. Chrome has likewise dropped support. It was never supported by IE. Thus, you can't use <keygen/> today, but this article will look at how it worked.

How did it work? At its core, the <keygen/> tag is simply a form control, which is given a name="" attribute like any other form control. A public key (for example, an RSA public key) is generated and sent in the field when the form is submitted, so the server can learn the web browser's public key.

The trick, as it were, is that when the public key is generated and submitted by the web browser, the corresponding private key is stored in a local database by the web browser — indeed, if the private key were simply thrown away, there would be little point.

In general, the public key sent to the server is then used by the server to generate a client certificate, which is delivered back to the user. The obvious way to do this is to redirect the user to a page where they can follow a link to the certificate file, but in principle a client certificate could be delivered at this point using some other non-web method.

When the user clicks a link to this X.509 client certificate in their browser, a special thing happens: the browser asks if the user wants to install the client certificate. When the browser tries to install the certificate, it discovers that it has the corresponding private key which it saved in a local database earlier, when the <keygen/> tag was submitted. This completes the enrolment process, and allows the user to access websites which require a client certificate.

It's interesting to note that the private key stored locally by the <keygen/> tag isn't visible to the user in any of Firefox's certificate or key management UI. As far as I can tell, there's no way to identify the presence of these locally stored private keys, or delete them.

UI issues. The UI issues with <keygen/> were severe, but probably the worst of these occurs not even during enrolment, but after it. A TLS web server can configure itself either to not request a client certificate (most websites), to request but not require one, or to require one. When visiting a website which requests or requires a client certificate, Netscape-heritage browsers would pop up a dialog asking you to choose what client certificate to use. If the website requires a client certificate but you don't have any suitable ones installed in the browser, the connection would simply fail with an SSL error screen.

Thus, if a website were configured to require a client certificate, the UI is terrible for users not yet enrolled, who get only an SSL error screen. There is no opportunity for websites to provide a message like “you have not registered yet, go here”. Since some website needs to be available to new users to handle enrolment, this basically requires you to have two different domains — say, newusers.example.com and secure.example.com, with only the latter requiring a certificate. Of course, if the user accidentally loses their client certificate or tries to access the secure site from a new device, they again get an SSL error which is extremely uninformative and unhelpful.

The UI if a website is configured to only request an optional client certificate is not that much better. While this allows successful access to the website for new users, the UI issues are similar to those of HTTP authentication; the user is prompted to choose a client certificate to send (if they have one) using a browser-specific UI, and this choice is then cached. Of course, most users will probably not know what a client certificate is, so this dialog is in itself confusing. Moreover, this creates an issue when the user completes enrolment and wishes to change from not using a client certificate to using a client certificate, as (much like browsers do not provide any way to clear credentials submitted for HTTP authentication) there is no way to manually re-trigger the certificate selection dialog, short of (for example) restarting the browser; there is no “logout” mechanism. The same issue occurs if you have multiple client certificates and want to change between them.

It was possible to configure Firefox to select a client certificate automatically if possible (for example, if only one eligible client certificate was installed), or to always prompt the user.

Actual deployment. Actual deployment of client certificate authentication on the web is probably found mostly in internal intranets. Though the <keygen/> enrolment mechanism has been removed from browsers today, web browsers can still use client certificates installed manually.

There was, however, one real-world deployment of the <keygen/> element on a public website which I remember all too well — in large part because of the poor UI made dealing with the website a frequent headache. Namely, the certificate authority StartSSL, part of StartCom.

StartSSL was a CA which built its name on giving away free SSL certificates when everyone else was charging for them. It was essentially a poor man's Let's Encrypt before Let's Encrypt had even been conceived. It wasn't possible to obtain certificates automatically — instead, you had to use their website. StartSSL had decided not to use usernames and passwords, but client certificates as the basis of authentication to their website; presumably the fact that they were an SSL company made them overenthusiastic about adopting client certificates despite the terrible UI in web browsers.

The site used the <keygen/> element to complete enrolment in a client certificate. Once you obtained the certificate, if I recall correctly, you were transferred to another subdomain configured to require a client certificate. If you hadn't installed the client certificate properly, you got an SSL error. I remember dealing with this process going wrong often and having to troubleshoot Firefox's client certificate handling. All in all, the main thing I took away from StartSSL's site is that using client certificates for website authentication is barely viable even for a highly technical audience. The UI for using and enrolling client certificates was basically shipped in Netscape 4 and never changed or improved since, an unloved and forgotten feature. Of course this is a vicious circle; nobody uses client certificates since the UI is terrible, and nobody spends time improving the UI because nobody uses the feature.

Incidentally, StartSSL was given the CA death penalty in 2016; you can find the details on Wikipedia.

What does it look like? I installed Firefox 1.0.8 (released 2006) under Wine to demonstrate <keygen/>:

(Yes, the <blink> element wasn't the only circumstance in which Netscape-heritage browsers would blink at you! The “please wait” dialog was likely included due to the significantly longer time key generation might take back when the feature was first implemented. Note also that you can't even select an RSA key size greater than 2048 bits.)

After the key generation is complete, the form is submitted as usual. The field data submitted for the <keygen/> field is a base64-encoded ASN.1 structure which is described on the MDN page. This format is known as the Netscape Signed Public Key and Challenge (SPKAC) format. OpenSSL has support for dumping it, though somewhat peculiarly, it can only handle it if provided in Base64-encoded form prefixed with the string SPKAC=:

$ (echo -n 'SPKAC='; cat test.der | base64 -w 0) | openssl spkac
Netscape SPKI:
  Public Key Algorithm: rsaEncryption
    RSA Public-Key: (2048 bit)
    Modulus:
        00:e2:8b:a3:c2:75:1d:d8:ee:eb:63:4a:8a:3c:f1:
        74:b6:4c:3a:90:bf:c7:eb:d6:6f:0f:e3:78:8f:91:
        b2:62:76:05:d5:cd:80:e5:ec:0a:bc:a0:1b:13:74:
        a1:28:79:8c:f6:4a:4c:1f:87:90:51:50:a0:b6:2e:
        13:a4:50:e0:1d:72:a1:87:ed:93:4c:f5:9b:e1:d9:
        3c:e6:61:30:50:f3:0f:0f:b4:b1:1a:0f:93:8c:54:
        e2:5f:bb:17:30:80:71:d8:bf:da:79:04:e0:20:9f:
        c5:18:bd:1e:42:9e:99:69:5c:22:0e:42:ff:86:8e:
        b8:b0:09:9a:e3:7d:f2:4e:2a:b7:ea:75:84:46:5c:
        3f:a0:60:b6:bb:f0:0c:81:c0:cf:ff:19:bd:18:b4:
        7a:a3:dd:09:55:a9:b7:8f:e3:1c:d5:42:86:e5:ce:
        ee:2d:80:5b:ef:d4:6c:61:fd:cb:1c:01:b2:02:51:
        e8:38:3a:d3:74:04:13:d2:de:30:83:91:50:3e:51:
        62:fd:e3:59:4e:d4:88:4a:6f:17:5c:4b:51:bd:ff:
        66:df:d1:b7:37:87:02:e9:c1:06:74:38:86:c6:ca:
        c6:53:61:b1:b0:cc:f9:08:fd:95:08:bd:e2:0f:2b:
        41:a6:68:68:e8:2b:5c:13:e6:0e:00:90:26:e1:ef:
        4d:93
    Exponent: 65537 (0x10001)
  Challenge String: example challenge string
  Signature Algorithm: md5WithRSAEncryption
      64:c1:e7:2d:57:c5:57:1d:df:79:45:94:47:c2:31:1f:77:3f:
      a9:f7:b8:78:e1:13:12:5d:2b:0c:89:3d:fd:a6:9b:e7:38:87:
      ee:b9:c9:54:66:29:1c:2c:35:c0:ae:f6:6d:57:d7:6c:2d:6f:
      ed:83:38:ae:e5:7e:84:a3:35:8a:9c:95:8a:4b:b1:04:a3:37:
      45:09:5f:82:ba:77:7e:4b:f2:a4:41:2e:1f:8a:1c:d9:2f:0b:
      ab:11:6d:fd:87:07:f9:0f:75:ad:28:4f:f9:14:c3:12:10:9e:
      71:8d:a4:59:22:7b:f7:d2:67:86:e3:76:13:31:ef:a9:ce:bb:
      2f:65:cd:b6:2d:b1:4a:78:38:df:53:f9:d8:17:89:92:9a:1e:
      bf:9e:62:a3:be:85:63:98:22:13:ef:e6:78:f6:ec:a6:9a:35:
      47:30:d9:a9:9a:68:02:1f:f0:37:12:55:e9:b3:6f:8f:07:43:
      5a:5a:a1:99:bb:07:30:58:28:1a:74:9e:1d:42:80:f7:49:8b:
      a5:4d:8e:94:08:cb:be:fa:b9:b5:41:5f:22:f1:f6:ba:3a:a9:
      3d:14:c3:44:10:3d:2e:36:03:99:38:d1:27:39:d9:c3:ca:c9:
      fe:ee:76:9e:ee:c8:40:1e:c8:b7:d7:79:0a:9f:c3:87:53:34:
      b5:1b:7f:e8

The challenge string comes from the challenge="" attribute of the <keygen/> element.

Internet Explorer. <keygen/>, like <embed> and <blink>, was a proprietary tag invented by Netscape, and was never supported by IE. Instead, IE required the use of an ActiveX object via JavaScript (new ActiveXObject("X509Enrollment.CX509EnrollmentWebClassFactory")) to obtain equivalent functionality.

Current situation. <keygen/> support has been removed from all modern web browsers. The MDN docs provide a good overview of what the element did. The HTML5 specification briefly documented <keygen/>, although it no longer does and recommends the use of the Web Cryptography API instead. Around 2011, a proposal for “TLS Origin-Bound Certificates (OBC)” was floated which was essentially a proposal for a variant of client certificates with better browser UI as a replacement for cookies, but this never saw wide adoption. It was later superceded by a “TLS Channel ID” proposal which also failed to gain adoption.

It is worth noting that TLS client certificates completely break corporate firewalls which seek to “secure” traffic by performing man-in-the-middle attacks on it. These systems generally work by installing a rogue CA in enterprise-controlled end devices to allow for traffic interception. However, the firewall can't forge a client certificate, which is after all validated by the website being connected to. Personally I see this as a feature2, but it probably also has served to inhibit further deployment of client certificates.

Nowadays, current efforts to advance the use of cryptography on the web seem to be focused on replacing passwords with hardware or virtual tokens using APIs such as WebAuthn. There is a clear parallel with client certificates here, being that the need for passwords is reduced or eliminated, but since the feature is a subject of modern interest and increasing trends rather than an unloved bygone, it has much better UI, usability, and browser support.

Further reading. This article by Simon Tatham provides an interesting discussion about how the functionality of the <keygen/> element can be emulated in browsers today. The WHATWG blog tag for keygen provides a chronicle of the abortive effort to standardise <keygen/> and links to many mailing list discussions that went on during this process, as well as a writeup of how <keygen/> works.


1. For example, client certificates are intrinsic to the EAP-TLS authentication method used to authenticate to “enterprise” Wi-Fi networks using EAP. EAP-TLS uses the TLS handshake for mutual authentication but does not actually even send anything over the established TLS channel, which is simply torn down upon a successful handshake, as this is in itself evidence of mutual authentication. A drawback of EAP-TLS is that, before TLS 1.3, client certificates were not encrypted, leading to privacy concerns. This led to the curious workaround of tunnelling TLS inside TLS (known as PEAP-EAP-TLS), with the client certificate only being presented on the inner TLS connection. TLS 1.3 dramatically renovated TLS, and client certificates are now always sent encrypted, which should render this unnecessary.

2. When browsers added support for certificate public key pinning (HPKE), a feature which has now subsequently been removed, the major browser vendors designed this feature to be bypassed automatically if a website was authenticated using a manually installed CA certificate, for the seemingly express purpose of not breaking these corporate MitM setups and ensuring that they kept working. I can't say this is a decision I agree with. In doing this the browser vendors essentially gave tacit consent to the widespread and systematic compromise of TLS inside organisations. In Google Chrome's case it was also a hypocritical move, as they exempted some traffic made by Google Chrome itself to Google servers from this bypass, meaning that Google's key pinning really was enforced for connections back to its own servers, preventing corporate MitM from working. Security for Google, but not for you.