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 fingerprint of the key you wish to query. The API supports querying based on the SPKI fingerprint, or (for RSA keys) the modulus fingerprint.

  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 key fingerprint

A key fingerprint is a value that uniquely identifies the key amongst all possible keys in the world. For our purposes, it is the SHA-256 hash of some part of the key.

All key types support what’s referred to as an “SPKI fingerprint”, which is a hash of the subjectPublicKeyInfo of the key. RSA keys can also be queried by the “modulus fingerprint”, which is a hash of the public modulus (or n) of the RSA key.

Which fingerprint you use is up to you, or your local trusted cryptographer.

Calculating an SPKI fingerprint

The SPKI fingerprint of a key 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.

Calculating a modulus fingerprint

RSA keys have a value called the “modulus” (often referred to as n, after its identifier in the mathematical representation of the RSA algorithm). This is one of the two values of interest in an RSA public key, (the other being e, the “public exponent”).

The thing about an RSA key is that basically every key uses the same e (65,537), and only the n changes. Also, you can use the same n with a different e, and get what is, technically, a different key (it will have a different SPKI fingerprint). However, if a key with a given value of n is compromised, all keys with that n are compromised, regardless of the value of e, because the only secret component of an RSA key is p and q – and n is just the product of p and q. Hence, if you’re looking up RSA keys, and want to be super sure about the key you’re checking, you should query the modulus fingerprint.

A modulus fingerprint, for Pwnedkeys’ purposes, is just the DER-encoded integer representing the value of n, hashed with SHA-256, represented as all-lowercase hex. Bear in mind that the DER-encoded integer includes the DER type byte, \x02.

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.

HTTP Response Status Codes

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).

To protect availability, you may at times be rate-limited, and your request will receive a 429 (Too Many Requests) response. All such responses will include a Retry-After header, indicating the number of seconds until requests will again succeed. Please wait at least that long before retrying any request; persistent failure to do say may land your IP address in the DROP list. If you would like an API key that will permit higher rate limits, please contact us.

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 or 429). Fix whatever was wrong, and try again.

Finally, while if there is a server-side problem, you will receive a 5xx series response code. Give it a second or two, and then try again. A connection failure, premature disconnection, or response time out also counts as a “server-side” problem, although the fault might not be at our end, it might be caused by networking problems in the middle.

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 that you’re querying. This is important, because the key embedded in the CSR itself could potentially be different from the key that signed the CSR.

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.