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:
-
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.
-
Make a request to
https://v1.pwnedkeys.com/<fingerprint>
. If the response to the request is a200 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. -
(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.
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.
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.
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:
-
a JSON Web Signature (”JWS”); or
-
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:
-
the
Accept
request header. Ifapplication/pkcs10
is preferred overapplication/json
, then you’ll get a CSR. -
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 yourAccept
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 beRS256
(an RSASSA-PKCS1-v1_5 signature over a SHA-256 hash), whilst for elliptic curve keys, you’ll get one ofES256
(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), orES512
(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.