Skip to content

Instantly share code, notes, and snippets.

@tomholub
Last active August 24, 2019 20:54
Show Gist options
  • Save tomholub/b0392cd03e2aaba9e170e06777682ea4 to your computer and use it in GitHub Desktop.
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
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