Add easywks import CLI command

This commit is contained in:
s3lph 2022-11-26 19:00:16 +01:00
parent 0a337f90ee
commit 492b823215
6 changed files with 51 additions and 6 deletions

View file

@ -303,6 +303,19 @@ Encrypt live everybody is.
``` ```
## Manual Key Import
In addition to WKS, EasyWKS also provides a command line interface for
importing keys from standard input. This feature is mainly intended
to be used for technical email accounts where using WKS might prove to
be difficult:
```console?prompt=$, 
$ cat pubkey.asc | easywks import
Skipping foreign email john.doe@notmydepartment.org
Imported key A58D3221F8079F35FF084890505A563492A56583 for email john.doe@example.org
```
[wkd]: https://wiki.gnupg.org/WKD [wkd]: https://wiki.gnupg.org/WKD
[wks]: https://wiki.gnupg.org/WKS [wks]: https://wiki.gnupg.org/WKS
[ietf]: https://datatracker.ietf.org/doc/html/draft-koch-openpgp-webkey-service-12 [ietf]: https://datatracker.ietf.org/doc/html/draft-koch-openpgp-webkey-service-12

View file

@ -96,7 +96,7 @@ def remove_pending_key(domain, nonce):
os.unlink(keyfile) os.unlink(keyfile)
def clean_stale_requests(): def clean_stale_requests(args):
stale = (datetime.utcnow() - timedelta(seconds=Config.pending_lifetime)).timestamp() stale = (datetime.utcnow() - timedelta(seconds=Config.pending_lifetime)).timestamp()
for domain in Config.domains: for domain in Config.domains:
pending_dir = os.path.join(Config.working_directory, domain, 'pending') pending_dir = os.path.join(Config.working_directory, domain, 'pending')

View file

@ -38,7 +38,7 @@ def hu(domain: str, userhash: str):
abort(404, 'Not Found') abort(404, 'Not Found')
def run_server(): def run_server(args):
run(host=Config.httpd['host'], port=Config.httpd['port']) run(host=Config.httpd['host'], port=Config.httpd['port'])

View file

@ -34,7 +34,7 @@ class LmtpdController(Controller):
return LMTP(handler=self.handler, ident=f'EasyWKS {version}', loop=self.loop) return LMTP(handler=self.handler, ident=f'EasyWKS {version}', loop=self.loop)
def run_lmtpd(): def run_lmtpd(args):
controller = LmtpdController(handler=LmtpMailServer(), hostname=Config.lmtpd['host'], port=Config.lmtpd['port']) controller = LmtpdController(handler=LmtpMailServer(), hostname=Config.lmtpd['host'], port=Config.lmtpd['port'])
controller.start() controller.start()
asyncio.get_event_loop().run_forever() asyncio.get_event_loop().run_forever()

View file

@ -1,7 +1,7 @@
from .config import Config from .config import Config
from .files import init_working_directory, clean_stale_requests from .files import init_working_directory, clean_stale_requests
from .process import process_mail_from_stdin from .process import process_mail_from_stdin, process_key_from_stdin
from .httpd import run_server from .httpd import run_server
from .lmtpd import run_lmtpd from .lmtpd import run_lmtpd
@ -32,6 +32,11 @@ def parse_arguments():
server = sp.add_parser('lmtpd', help='Run a LMTP server to receive mails from your MTA. Also see process.') server = sp.add_parser('lmtpd', help='Run a LMTP server to receive mails from your MTA. Also see process.')
server.set_defaults(fn=run_lmtpd) server.set_defaults(fn=run_lmtpd)
imp = sp.add_parser('import', help='Import a public key from stdin directly into the WKD without WKS verification.')
imp.add_argument('--uid', '-u', type=str, action='append',
help='Limit import to a subset of the key\'s UIDs. Can be provided multiple times.')
imp.set_defaults(fn=process_key_from_stdin)
return ap.parse_args(sys.argv[1:]) return ap.parse_args(sys.argv[1:])
@ -44,7 +49,7 @@ def main():
Config.load_config(conf) Config.load_config(conf)
init_working_directory() init_working_directory()
if args.fn: if args.fn:
args.fn() args.fn(args)
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -7,6 +7,7 @@ from .config import Config
from .files import read_pending_key, write_public_key, remove_pending_key, write_pending_key from .files import read_pending_key, write_public_key, remove_pending_key, write_pending_key
from .types import SubmissionRequest, ConfirmationResponse, PublishResponse, ConfirmationRequest, EasyWksError,\ from .types import SubmissionRequest, ConfirmationResponse, PublishResponse, ConfirmationRequest, EasyWksError,\
XLOOP_HEADER XLOOP_HEADER
from .types import fingerprint
from email.message import MIMEPart, Message from email.message import MIMEPart, Message
from email.parser import BytesParser from email.parser import BytesParser
@ -172,6 +173,32 @@ def process_mail(mail: bytes):
method(rmsg) method(rmsg)
def process_mail_from_stdin(): def process_mail_from_stdin(args):
mail = sys.stdin.read().encode() mail = sys.stdin.read().encode()
process_mail(mail) process_mail(mail)
def process_key_from_stdin(args):
try:
pubkey, _ = PGPKey.from_blob(sys.stdin.read())
except PGPError:
raise EasyWksError('Input is not a valid public key.')
if not pubkey.is_public:
raise EasyWksError('Input is not a valid public key.')
for uid in pubkey.userids:
# Skip user attributes (e.g. photo ids)
if not uid.is_uid or len(uid.email) == 0:
continue
# If a UID filter was provided on the command line, apply it
if args.uid is not None and len(args.uid) > 0 and uid.email not in args.uid:
print(f'Skipping ignored email {uid.email}')
continue
local, domain = uid.email.split('@', 1)
# Skip keys we're not responsible for
if domain not in Config.domains:
print(f'Skipping foreign email {uid.email}')
continue
# All checks passed, importing key
write_public_key(domain, uid.email, pubkey)
print(f'Imported key {fingerprint(pubkey)} for email {uid.email}')