Last active
August 24, 2019 20:54
-
-
Save tomholub/b0392cd03e2aaba9e170e06777682ea4 to your computer and use it in GitHub Desktop.
WKD (Web Key Directory) client for fetching OpenPGP public keys in python3, including a primitive z-base-32 implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import requests | |
import requests.adapters | |
import requests.exceptions | |
from ssl import SSLError | |
import hashlib | |
# https://datatracker.ietf.org/doc/draft-koch-openpgp-webkey-service/?include_text=1 | |
# https://www.sektioneins.de/en/blog/18-11-23-gnupg-wkd.html | |
class PubkeySource: | |
_request_err = (requests.ConnectionError, requests.adapters.ReadTimeout, requests.adapters.ConnectTimeout, SSLError, requests.exceptions.Timeout) | |
def _email_as_user_and_domain(self, email: str): | |
user, domain = email.strip().lower().split('@') | |
return user, domain | |
class Wkd(PubkeySource): | |
__TIMEOUT = (3, 3) | |
def find_pubkey(self, email:str=None): | |
user, domain = self._email_as_user_and_domain(email) | |
userhash = self.__sha1_zbase32(user) | |
url_advanced = f'https://openpgpkey.{domain}/.well-known/openpgpkey/{domain}/hu/{userhash}?l={user}' | |
url_direct = f'https://{domain}/.well-known/openpgpkey/hu/{userhash}?l={user}' | |
return self.__try_url(url_advanced) or self.__try_url(url_direct) | |
def __try_url(self, url: str): | |
try: | |
r = requests.get(url, timeout=self.__TIMEOUT) | |
if r.status_code != 200: | |
return None | |
if r.headers['Content-Type'] == 'text/html' and ("404" in r.text or "Not Found" in r.text): | |
return None # likely a catchall 404 html page, even if responded with 200 | |
return r.content | |
except self._request_err: | |
return None | |
except (ValueError, KeyError): | |
return None | |
def __sha1_zbase32(self, user: str): | |
def chunk_string(string, length): | |
return (string[0 + i:length + i] for i in range(0, len(string), length)) | |
def chars_from_int(int32): | |
result = [] | |
shift = 27 | |
for _ in range(4): | |
result.append("ybndrfg8ejkmcpqxot1uwisza345h769"[(int32 >> shift) & 31]) | |
shift = shift - 5 | |
return result | |
hashed_bytes = hashlib.sha1(user.encode()).digest() | |
bits_string = bin(int.from_bytes(hashed_bytes, byteorder="big"))[2:] | |
chunks_of_20_bits = chunk_string(bits_string, 20) | |
offset_chunks_of_20_bits = [chunk + '000000000000' for chunk in chunks_of_20_bits] | |
ints = [int(bits, 2) for bits in offset_chunks_of_20_bits] | |
groups_of_4_chars = [chars_from_int(int32) for int32 in ints] | |
return "".join(sum(groups_of_4_chars, [])) | |
wkd = Wkd() | |
print(wkd.find_pubkey("[email protected]")) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment