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-2048 (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:
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.
Make a request to
https://v1.pwnedkeys.com/<fingerprint>. If the response to the request is a
200 OK, then the key appears in the database, while a 404 response indicates that the key doesn’t appear in the pwnedkeys database.
(Optional) Verify that pwnedkeys does, indeed, have possession of the private key, by validating the JSON Web Signature returned in the response to the API request.
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
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
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
Sidebar – SPKI: The Gory Bits
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.
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
subjectPublicKey should be the DER-encoded sequence containing the
n) and public exponent (
e) of the key. The algorithm identifier
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
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
GET requests to a URL of the following form:
<fingerprint> is the all-lowercase, hex-encoded fingerprint of the key
you wish to query for. The HTTP request
Accept header must allow responses
application/json content type, because that’s what’s going to
If the queried key exists in the pwnedkeys database, the HTTP response code
OK), and the response body will be a JSON Web Signature which
you can use to verify that the key really is pwned, and we’re not just making
things up (more on that in the next section). If the key is not in the
pwnedkeys database, then the response code will be
If there was a problem with your request (you didn’t provide a valid key
fingerprint, for instance) you will receive a
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
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 in the pwnedkeys database will include a JSON Web Signature object which has been signed with the private key being queried. Thus, if you have the corresponding public key (from a CSR or certificate, for instance) you can validate the signature and be quite sure that the key is, in fact, pwned.
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,
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.