The pwnedkeys v1 query API allows you to answer the question, “has the private key associated with this public key been publicly exposed?”. You need to have an RSA-1024 (or larger) or ECDSA public key available, and the ability to make an HTTPS request to https://v1.pwnedkeys.com over IPv4 or IPv6.

Making a query to the pwnedkeys v1 API involves the following steps:

  1. Calculate the SPKI fingerprint of the key you wish to query. This may involve converting the key from some other format into the SPKI data structure, but this is usually relatively straightforward.

  2. Make a request to https://v1.pwnedkeys.com/<fingerprint>. If the response to the request is a 200 OK, then the key is known to be compromised, while a 404 response indicates that the key doesn’t appear in the pwnedkeys database, and thus is not currently known to be compromised.

  3. (Optional) Verify that pwnedkeys isn’t making things up, by validating that the response body has a signature from the compromised key, proving that the private key was used.

Let’s examine each of those steps in more detail.

Step 1: Calculate the SPKI fingerprint

The SPKI fingerprint of a key (or certificate) is the all-lowercase hex-encoded SHA-256 hash of the DER-encoded form of the subjectPublicKeyInfo ASN.1 structure representing a given public key. Try saying that ten times fast.

The SPKI data structure contains an algorithm identifier, some options, and some sort of representation of the key’s unique parameters (such as modulus/public exponent for RSA keys, or a curve point for ECDSA keys). These are the same values as are required for any form of a public key, it’s just that they’re all jammed into an ASN.1 structure when they’re in the SPKI form.

Taking the “fingerprint” of the SPKI data structure is nothing more than calculating the SHA-256 hash of the encoded SPKI data, and then representing that hash as a hex-encoded string.

Many programming languages have libraries which make the generation and calculation of an SPKI fingerprint straightforward. Just be careful when you’re choosing a function to call that it is (a) hex-encoded, (b) a SHA-256 hash (rather than SHA-1 or something else), and (c) taken over the DER-encoded subjectPublicKeyInfo structure, and not a PEM format, just the subjectPublicKey, or the entire certificate / CSR / whatever.

For quick testing, you can extract the SHA-256 fingerprint from a private key, CSR, or certificate using the following command line (adapted from the Mozilla documentation):

For an RSA private key:

openssl rsa -in rsa-key.pem -outform der -pubout | openssl dgst -sha256 -hex

For an EC private key:

openssl ec -in ec-key.pem -outform der -pubout | openssl dgst -sha256 -hex

For a CSR:

openssl req -in csr.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -hex

And, finally, for an X.509 certificate:

openssl x509 -in cert.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -hex

If you want a double-check of your SPKI fingerprint calculation code, try feeding your code the example keys and ensuring that the fingerprint you calculate matches the ones listed.

This section is only important if you need to implement SPKI assembly and fingerprint calculation in an environment which doesn’t have any existing means of doing so. Otherwise you can remain blissfully unaware of these dark arts.

The subjectPublicKeyInfo structure is, in and of itself, fairly straightforward – a sequence of an algorithm specifier (itself a sequence) and a string containing the actual key data. However, since every different type of key has its own ideas about what goes into a public key, and the parameters of an algorithm can vary quite considerably, it can get a bit fiddly until you’re used to it.

For RSA keys, RFC3279 says the subjectPublicKey should be the DER-encoded sequence containing the modulus (n) and public exponent (e) of the key. The algorithm identifier is just rsaEncryption, and the parameters sequence element MUST be an ASN1 NULL.

EC keys are a bit trickier. RFC5480 is the full reference, but just quickly, you need to set the algorithm identifier to id-ecPublicKey, while the algorithm parameters is just an object ID specifying which particular curve is being used (there are a lot of possibilities in the RFC, but really only a few ever get used). The subjectPublicKey is a direct encoding of the “point” which represents the public key. One annoyance of EC keys is that the same point can be stored in “compressed” or “uncompressed” form, which results in two different fingerprints. To avoid potential problems, pwnedkeys actually calculates both fingerprints and will respond to a request for the fingerprint of either. You’re welcome, world.

Step 2: Query the pwnedkeys API

To ask the pwnedkeys API whether a key is in the pwnedkeys database, you make a GET requests to a URL of the following form:

https://v1.pwnedkeys.com/<fingerprint>

Where <fingerprint> is the all-lowercase, hex-encoded fingerprint of the key you wish to query for.

If the queried key exists in the pwnedkeys database, the HTTP response code will be 200 (OK). If the key is not in the pwnedkeys database, then the response code will be 404 (Not Found).

If there was a problem with your request (you didn’t provide a valid key fingerprint, for instance) you will receive a 400 (Bad Request) response code (or another 4xx series code other than 404), while if there is a server-side problem, you will receive a 5xx series response code. A connection failure, premature disconnection, or response time out is also a server-side problem.

Response Body Format

The v1 API can provide responses in two formats:

  1. a JSON Web Signature (“JWS”); or

  2. a PKCS#10 Certificate Signing Request (“CSR”).

By default, because it’s (marginally) more human-readable, a JWS will be returned. However, you can control what format or response you receive by one of these methods:

  1. the Accept request header. If application/pkcs10 is preferred over application/json, then you’ll get a CSR.

  2. the resource extension. If you put a .csr or .p10 on the end of the fingerprint (eg. https://v1.pwnedkey.com/abcd123[...]ef4321.csr) then you’ll get a CSR, while .json or .jws will force a JWS response. This overrides anything in your Accept header, BTW.

Step 3: Validate the Response

Whilst pwnedkeys is a very trustworthy service, and you should definitely trust what the API says, it’s still a good idea to verify that the key is, in fact, pwned, and we’re not just saying everything’s terrible.

To help you with that, every response that indicates the queried key is compromised will include an object signed by the private key that has been pwned. That signature proves that the private key is pwned, because if we didn’t have the private key, we couldn’t make that signature.

If you request a CSR response from the API, then validating it is a matter of verifying that the signature on the CSR was generated by the key embedded in the CSR (you can do that with openssl req -inform der -verify -noout), and then double-checking that the public key in the CSR matches the key you’re checking.

If you’ve asked for a JWS, that’s slightly more involved, because there aren’t widely-available third-party tools for working with JWSes. Whilst you can read the RFC and figure out what’s going on, the following description of what a pwnedkeys API response looks like should be enough to get you going. If you’re familiar with JWS, the short version is that we’re using a flattened JSON encoding with only a protected header. If you’re not a JWS afficionado, read on.

The HTTP response as a whole is a JSON object – that is, a bunch of "key":"value" pairs wrapped in braces. It contains the following keys:

  • "protected" – a string containing a URL-safe, base64-encoded, JSON object which is itself the “protected headers” of the signature. The keys in this sub-object will be:

    • "alg" – the signature algorithm used to generate the signature. Which algorithm is used for signing is determined by the type of key that was looked up. For RSA keys, alg will be RS256 (an RSASSA-PKCS1-v1_5 signature over a SHA-256 hash), whilst for elliptic curve keys, you’ll get one of ES256 (an ECDSA signature using a P-256 key with a SHA-256 hash), ES384 (an ECDSA signature using a P-384 key with a SHA-384 hash), or ES512 (an ECDSA signature using a P-521 key with a SHA-512 hash).

      References to the exact details of each of these schemes can be found in the relevant IANA registry.

      Note that this algorithm list is not exhaustive, and may be extended over time as more key types are supported by the pwnedkeys database. However, since you should only ever be getting a signature matching the type of key you are querying for, you shouldn’t get a signature algorithm you’re not prepared to handle.

    • "kid" – this is the hex-encoded, SHA-256 fingerprint of the queried key, which is the same as the fingerprint used to query the key.

  • payload – a string containing a URL-safe, base64-encoded string, which attests that the key has, in fact, been pwned. Whilst the exact contents of the payload are subject to change, it is guaranteed to contain the ASCII string “key is pwned” somewhere, and be no more than 1024 bytes in length (before base64 encoding).

  • signature – a string containing a URL-safe, base64-encoded JWS signature, generated as per RFC7515 s5.1.

Note: a “URL-safe base64-encoded string” is just like a regular base64-encoded string, except the + is replaced with -, / is replaced with _, and the trailing “padding” equals signs are omitted.