#!/usr/bin/python3

# Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
# License: GPL v3+
# Date: 2019-03-21

# usage: build-wkd [DOMAIN_NAME [KEYRING ...]]

# Build a WKD directory for a given domain name, based on some set of
# keyrings.  The resultant openpgpkey/ directory is built in the
# current working directory.  Typically you would invoke this from an
# empty directory, as it needs to create a temporary GnuPG homedir for
# cleanliness.

# If arguments are not supplied, the default is to use
# DOMAIN=@debian.org, and the KEYRINGs used are the relevant ones from
# the debian-keyring package.

from hashlib import sha1
from subprocess import run, Popen, PIPE
from os import path, mkdir, rmdir, unlink
import codecs
from multiprocessing.pool import ThreadPool
import sys

from typing import List


def wkd_localpart(incoming: bytes) -> str:
    'see  https://tools.ietf.org/html/draft-koch-openpgp-webkey-service-07#section-3.1'

    # https://tools.ietf.org/html/rfc6189#section-5.1.6
    zb32 = "ybndrfg8ejkmcpqxot1uwisza345h769"

    b = sha1(incoming).digest()
    ret = ""
    assert(len(b)*8 == 160)
    for i in range(0, 160, 5):
        byte = i//8
        offset = i - byte*8
        #offset | bits remaining in k+1 | right-shift k+1
        # 3 | 0 | x
        # 4 | 1 | 7
        # 5 | 2 | 6
        # 6 | 3 | 5
        # 7 | 4 | 4
        if offset < 4:
            n = (b[byte] >> (3-offset))
        else:
            n = (b[byte] << (offset-3)) + (b[byte+1] >> (11-offset))

        ret += zb32[n & 0b11111]
    return ret


def getdomainlocalpart(line: bytes, domain: bytes) -> bytes:
    if line.startswith(b'uid:'):
        uid = line.split(b':')[9]
        if uid.endswith(b'@' + domain + b'>'):
            broken = uid.split(b'<')
            if len(broken) != 2:
                raise ValueError("unexpected User ID %s"%(uid))
            return broken[1][:-len(b'@' + domain + b'>')].lower()
    return None


def gpgbase(keyrings):
    return ['gpg', '--batch', '--no-options', '--with-colons',
            '--no-default-keyring',
            '--homedir=/dev/null', '--trust-model=always',
            '--fixed-list-mode'] + list(map(lambda k: '--keyring='+k, keyrings))

def emit_wkd(localpart: bytes, domain: bytes, keyrings: List):
    wkdstr = wkd_localpart(localpart)
    # what do we do if this local part is not a proper encoding?
    addr = codecs.decode(localpart) + '@' + domain
    cmd = gpgbase(keyrings) + ['--output', path.join('openpgpkey', 'hu',wkdstr),
                               '--export-options', 'export-minimal',
                               '--export-filter', 'keep-uid=mbox='+addr,
                               '--export', '<' + addr + '>']
    run(cmd)

def build_wkd(domain, keyrings):
    mkdir('openpgpkey')
    mkdir(path.join('openpgpkey', 'hu'))

    # FIXME: deal with IDN:
    bytedomain=codecs.encode(domain)
        
    lister = Popen(gpgbase(keyrings) + ['--list-keys', '@' + domain], stdout=PIPE)

    localparts = set(map(lambda x: getdomainlocalpart(x, bytedomain), lister.stdout))
    localparts.discard(None)

    def runner(x):
        return emit_wkd(x, domain, keyrings)

    pool = ThreadPool(None)
    for localpart in localparts:
        pool.apply_async(runner, (localpart,))

    pool.close()
    pool.join()
    # make the policy file:
    policyfile = open(path.join('openpgpkey', 'policy'), 'wb')
    del policyfile

if __name__ == '__main__':
    sys.argv.pop(0)
    if len(sys.argv):
        domain = sys.argv.pop(0)
    else:
        domain = 'debian.org'

    if len(sys.argv):
        keys = sys.argv
    else:
        keys = ['/usr/share/keyrings/debian-nonupload.gpg',
                '/usr/share/keyrings/debian-keyring.gpg',
                '/usr/share/keyrings/debian-role-keys.gpg']
    build_wkd(domain, keys)
