feat: allow submitting additional revoked keys with the submission request
This commit is contained in:
parent
fddbea70d9
commit
4e1465cdb2
5 changed files with 96 additions and 33 deletions
|
@ -8,7 +8,7 @@ from pgpy import PGPKey
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .crypto import create_pgp_key, privkey_to_pubkey
|
from .crypto import create_pgp_key, privkey_to_pubkey
|
||||||
from .util import hash_user_id
|
from .util import hash_user_id, armor_keys, split_revoked
|
||||||
|
|
||||||
|
|
||||||
def _locked_read(file: str, binary: bool = False):
|
def _locked_read(file: str, binary: bool = False):
|
||||||
|
@ -62,33 +62,34 @@ def init_working_directory():
|
||||||
|
|
||||||
|
|
||||||
def read_public_key(domain, user):
|
def read_public_key(domain, user):
|
||||||
hu = hash_user_id(user)
|
return read_hashed_public_key(domain, hash_user_id(user))
|
||||||
keyfile = os.path.join(Config.working_directory, domain, 'hu', hu)
|
|
||||||
key, _ = PGPKey.from_blob(_locked_read(keyfile, binary=True))
|
|
||||||
return key
|
|
||||||
|
|
||||||
|
|
||||||
def read_hashed_public_key(domain, hu):
|
def read_hashed_public_key(domain, hu):
|
||||||
keyfile = os.path.join(Config.working_directory, domain, 'hu', hu)
|
keyfile = os.path.join(Config.working_directory, domain, 'hu', hu)
|
||||||
key, _ = PGPKey.from_blob(_locked_read(keyfile, binary=True))
|
_, keys = PGPKey.from_blob(_locked_read(keyfile, binary=True))
|
||||||
return key
|
key, revoked = split_revoked(keys.values())
|
||||||
|
return key, revoked
|
||||||
|
|
||||||
|
|
||||||
def write_public_key(domain, user, key):
|
def write_public_key(domain, user, key, revoked):
|
||||||
hu = hash_user_id(user)
|
hu = hash_user_id(user)
|
||||||
keyfile = os.path.join(Config.working_directory, domain, 'hu', hu)
|
keyfile = os.path.join(Config.working_directory, domain, 'hu', hu)
|
||||||
_locked_write(keyfile, bytes(key), binary=True)
|
joined = bytes(key) + b''.join([bytes(k) for k in revoked])
|
||||||
|
_locked_write(keyfile, joined, binary=True)
|
||||||
|
|
||||||
|
|
||||||
def read_pending_key(domain, nonce):
|
def read_pending_key(domain, nonce):
|
||||||
keyfile = os.path.join(Config.working_directory, domain, 'pending', nonce)
|
keyfile = os.path.join(Config.working_directory, domain, 'pending', nonce)
|
||||||
key, _ = PGPKey.from_blob(_locked_read(keyfile))
|
_, keys = PGPKey.from_blob(_locked_read(keyfile))
|
||||||
return key
|
key, revoked = split_revoked(keys.values())
|
||||||
|
return key[0], revoked
|
||||||
|
|
||||||
|
|
||||||
def write_pending_key(domain, nonce, key):
|
def write_pending_key(domain, nonce, key, revoked_keys):
|
||||||
keyfile = os.path.join(Config.working_directory, domain, 'pending', nonce)
|
keyfile = os.path.join(Config.working_directory, domain, 'pending', nonce)
|
||||||
_locked_write(keyfile, str(key))
|
armored = armor_keys([key] + revoked_keys)
|
||||||
|
_locked_write(keyfile, armored)
|
||||||
|
|
||||||
|
|
||||||
def remove_pending_key(domain, nonce):
|
def remove_pending_key(domain, nonce):
|
||||||
|
|
|
@ -59,10 +59,10 @@ def advanced_hu(domain: str, userhash: str):
|
||||||
if not userid or hash_user_id(userid) != userhash:
|
if not userid or hash_user_id(userid) != userhash:
|
||||||
abort(404, 'Not Found')
|
abort(404, 'Not Found')
|
||||||
try:
|
try:
|
||||||
pubkey = read_hashed_public_key(domain, userhash)
|
pubkey, revoked = read_hashed_public_key(domain, userhash)
|
||||||
response.add_header('Content-Type', 'application/octet-stream')
|
response.add_header('Content-Type', 'application/octet-stream')
|
||||||
response.add_header(*CORS)
|
response.add_header(*CORS)
|
||||||
return bytes(pubkey)
|
return bytes(pubkey) + b''.join([bytes(k) for k in revoked])
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
abort(404, 'Not Found')
|
abort(404, 'Not Found')
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import sys
|
import sys
|
||||||
from typing import List, Dict
|
from typing import List, Dict, Tuple
|
||||||
|
|
||||||
from .crypto import pgp_decrypt
|
from .crypto import pgp_decrypt
|
||||||
from .mailing import get_mailing_method
|
from .mailing import get_mailing_method
|
||||||
|
@ -8,6 +8,7 @@ from .files import read_pending_key, write_public_key, remove_pending_key, write
|
||||||
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 .types import fingerprint
|
||||||
|
from .util import split_revoked
|
||||||
|
|
||||||
from email.message import MIMEPart, Message
|
from email.message import MIMEPart, Message
|
||||||
from email.parser import BytesParser
|
from email.parser import BytesParser
|
||||||
|
@ -46,30 +47,43 @@ def _get_pgp_message(parts: List[MIMEPart]) -> PGPMessage:
|
||||||
return pgp
|
return pgp
|
||||||
|
|
||||||
|
|
||||||
def _get_pgp_publickey(parts: List[MIMEPart]) -> PGPKey:
|
def _get_pgp_publickeys(parts: List[MIMEPart]) -> Tuple[PGPKey, List[PGPKey]]:
|
||||||
pubkey = None
|
pubkeys: Dict[str, PGPKey] = {}
|
||||||
for part in parts:
|
for part in parts:
|
||||||
try:
|
try:
|
||||||
key, _ = PGPKey.from_blob(part.get_content())
|
_, keys = PGPKey.from_blob(part.get_content())
|
||||||
if key.is_public:
|
for (_, public), key in keys.items():
|
||||||
if pubkey:
|
if not public:
|
||||||
raise EasyWksError('More than one PGP public key in message. Only submit a single key at once.')
|
continue
|
||||||
pubkey = key
|
fpr = fingerprint(key.fingerprint)
|
||||||
|
if fpr in pubkeys:
|
||||||
|
raise EasyWksError(f'Key with fingerprint {fpr} appears multiple times in submission request.')
|
||||||
|
pubkeys[fpr] = key
|
||||||
except PGPError:
|
except PGPError:
|
||||||
pass
|
pass
|
||||||
if not pubkey:
|
if len(pubkeys) == 0:
|
||||||
raise EasyWksError('No PGP public key found in the encrypted message part.')
|
raise EasyWksError('No PGP public key found in the encrypted message part.')
|
||||||
return pubkey
|
key, revoked_keys = split_revoked(pubkeys.values())
|
||||||
|
if len(key) < 1:
|
||||||
|
raise EasyWksError('All of the submitted keys appear to be revoked.')
|
||||||
|
elif len(key) > 1:
|
||||||
|
fprs = ' '.join([fingerprint(k) for k in key])
|
||||||
|
raise EasyWksError(f'More than one non-revoked key was submitted: {fprs}')
|
||||||
|
return key[0], revoked_keys
|
||||||
|
|
||||||
|
|
||||||
def _parse_submission_request(pgp: PGPMessage, submission: str, sender: str):
|
def _parse_submission_request(pgp: PGPMessage, submission: str, sender: str):
|
||||||
payload = BytesParser(policy=default).parsebytes(pgp.message)
|
payload = BytesParser(policy=default).parsebytes(pgp.message)
|
||||||
leafs = _get_mime_leafs(payload)
|
leafs = _get_mime_leafs(payload)
|
||||||
pubkey = _get_pgp_publickey(leafs)
|
valid_key, revoked_keys = _get_pgp_publickeys(leafs)
|
||||||
sender_uid: PGPUID = pubkey.get_uid(sender)
|
sender_uid: PGPUID = valid_key.get_uid(sender)
|
||||||
if sender_uid is None or sender_uid.email != sender:
|
if sender_uid is None or sender_uid.email != sender:
|
||||||
raise EasyWksError(f'Key has no UID that matches {sender}')
|
raise EasyWksError(f'Key has no UID that matches {sender}')
|
||||||
return SubmissionRequest(sender, submission, pubkey)
|
for key in revoked_keys:
|
||||||
|
sender_uid: PGPUID = key.get_uid(sender)
|
||||||
|
if sender_uid is None or sender_uid.email != sender:
|
||||||
|
raise EasyWksError(f'Revoked key {fingerprint(key)} has no UID that matches {sender}')
|
||||||
|
return SubmissionRequest(sender, submission, valid_key, revoked_keys)
|
||||||
|
|
||||||
|
|
||||||
def _find_confirmation_response(parts: List[MIMEPart]) -> str:
|
def _find_confirmation_response(parts: List[MIMEPart]) -> str:
|
||||||
|
@ -152,7 +166,7 @@ def process_mail(mail: bytes):
|
||||||
if confirmation_response is not None:
|
if confirmation_response is not None:
|
||||||
request: ConfirmationResponse = _parse_confirmation_response(decrypted, submission_address, sender_mail)
|
request: ConfirmationResponse = _parse_confirmation_response(decrypted, submission_address, sender_mail)
|
||||||
try:
|
try:
|
||||||
key = read_pending_key(sender_domain, request.nonce)
|
key, revoked_keys = read_pending_key(sender_domain, request.nonce)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise EasyWksError('There is no submission request for this email address, or it has expired. '
|
raise EasyWksError('There is no submission request for this email address, or it has expired. '
|
||||||
'Please resubmit your submission request.')
|
'Please resubmit your submission request.')
|
||||||
|
@ -160,13 +174,13 @@ def process_mail(mail: bytes):
|
||||||
request.verify_signature(key)
|
request.verify_signature(key)
|
||||||
response: PublishResponse = request.get_publish_response(key)
|
response: PublishResponse = request.get_publish_response(key)
|
||||||
rmsg = response.create_signed_message()
|
rmsg = response.create_signed_message()
|
||||||
write_public_key(sender_domain, sender_mail, key)
|
write_public_key(sender_domain, sender_mail, key, revoked_keys)
|
||||||
remove_pending_key(sender_domain, request.nonce)
|
remove_pending_key(sender_domain, request.nonce)
|
||||||
else:
|
else:
|
||||||
request: SubmissionRequest = _parse_submission_request(decrypted, submission_address, sender_mail)
|
request: SubmissionRequest = _parse_submission_request(decrypted, submission_address, sender_mail)
|
||||||
response: ConfirmationRequest = request.confirmation_request()
|
response: ConfirmationRequest = request.confirmation_request()
|
||||||
rmsg = response.create_signed_message()
|
rmsg = response.create_signed_message()
|
||||||
write_pending_key(sender_domain, response.nonce, request.key)
|
write_pending_key(sender_domain, response.nonce, request.key, request.revoked_keys)
|
||||||
except EasyWksError as e:
|
except EasyWksError as e:
|
||||||
rmsg = e.create_message(sender_mail, submission_address)
|
rmsg = e.create_message(sender_mail, submission_address)
|
||||||
method = get_mailing_method(Config.mailing_method)
|
method = get_mailing_method(Config.mailing_method)
|
||||||
|
@ -200,5 +214,5 @@ def process_key_from_stdin(args):
|
||||||
print(f'Skipping foreign email {uid.email}')
|
print(f'Skipping foreign email {uid.email}')
|
||||||
continue
|
continue
|
||||||
# All checks passed, importing key
|
# All checks passed, importing key
|
||||||
write_public_key(domain, uid.email, pubkey)
|
write_public_key(domain, uid.email, pubkey, [])
|
||||||
print(f'Imported key {fingerprint(pubkey)} for email {uid.email}')
|
print(f'Imported key {fingerprint(pubkey)} for email {uid.email}')
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from email.encoders import encode_noop
|
from email.encoders import encode_noop
|
||||||
from email.policy import default
|
from email.policy import default
|
||||||
|
@ -20,10 +22,11 @@ XLOOP_HEADER = 'EasyWKS'
|
||||||
|
|
||||||
class SubmissionRequest:
|
class SubmissionRequest:
|
||||||
|
|
||||||
def __init__(self, submitter_addr: str, submission_addr: str, key: PGPKey):
|
def __init__(self, submitter_addr: str, submission_addr: str, key: PGPKey, revoked_keys: List[PGPKey]):
|
||||||
self._submitter_addr = submitter_addr
|
self._submitter_addr = submitter_addr
|
||||||
self._submission_addr = submission_addr
|
self._submission_addr = submission_addr
|
||||||
self._key = key
|
self._key = key
|
||||||
|
self._revoked_keys = revoked_keys
|
||||||
|
|
||||||
def confirmation_request(self) -> 'ConfirmationRequest':
|
def confirmation_request(self) -> 'ConfirmationRequest':
|
||||||
return ConfirmationRequest(self._submitter_addr, self. _submission_addr, self._key)
|
return ConfirmationRequest(self._submitter_addr, self. _submission_addr, self._key)
|
||||||
|
@ -40,6 +43,10 @@ class SubmissionRequest:
|
||||||
def key(self):
|
def key(self):
|
||||||
return self._key
|
return self._key
|
||||||
|
|
||||||
|
@property
|
||||||
|
def revoked_keys(self):
|
||||||
|
return list(self._revoked_keys)
|
||||||
|
|
||||||
|
|
||||||
class ConfirmationRequest:
|
class ConfirmationRequest:
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
|
|
||||||
|
from typing import Iterable, List, Tuple
|
||||||
|
|
||||||
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from pgpy import PGPKey
|
from pgpy import PGPKey
|
||||||
|
from pgpy.constants import SignatureType
|
||||||
|
|
||||||
|
|
||||||
def _zrtp_base32(sha1: bytes) -> str:
|
def _zrtp_base32(sha1: bytes) -> str:
|
||||||
|
@ -36,3 +41,39 @@ def create_nonce(n: int = 32) -> str:
|
||||||
|
|
||||||
def fingerprint(key: PGPKey) -> str:
|
def fingerprint(key: PGPKey) -> str:
|
||||||
return key.fingerprint.upper().replace(' ', '')
|
return key.fingerprint.upper().replace(' ', '')
|
||||||
|
|
||||||
|
|
||||||
|
def crc24(data: bytes) -> bytes:
|
||||||
|
# https://www.rfc-editor.org/rfc/rfc4880#section-6.1
|
||||||
|
crc = 0xB704CE
|
||||||
|
for b in data:
|
||||||
|
crc ^= (b << 16)
|
||||||
|
for _ in range(8):
|
||||||
|
crc <<= 1
|
||||||
|
if crc & 0x1000000:
|
||||||
|
crc ^= 0x1864CFB
|
||||||
|
return bytes([(crc & 0xff0000) >> 16, (crc & 0xff00) >> 8, (crc & 0xff)])
|
||||||
|
|
||||||
|
|
||||||
|
def armor_keys(keys: List[PGPKey]) -> str:
|
||||||
|
joined = b''.join([bytes(k) for k in keys])
|
||||||
|
armored = base64.b64encode(joined).decode()
|
||||||
|
wrapped = '\n'.join(textwrap.wrap(armored, 64))
|
||||||
|
checksum = base64.b64encode(crc24(joined)).decode()
|
||||||
|
armored = '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n' +\
|
||||||
|
wrapped + '\n=' + checksum +\
|
||||||
|
'\n-----END PGP PUBLIC KEY BLOCK-----\n'
|
||||||
|
return armored
|
||||||
|
|
||||||
|
|
||||||
|
def split_revoked(keys: Iterable[PGPKey]) -> Tuple[List[PGPKey], List[PGPKey]]:
|
||||||
|
revoked_keys = set()
|
||||||
|
for key in keys:
|
||||||
|
if len(key.revocation_signatures) == 0:
|
||||||
|
continue
|
||||||
|
for rsig in key.revocation_signatures:
|
||||||
|
if rsig.type == SignatureType.KeyRevocation:
|
||||||
|
revoked_keys.add(key)
|
||||||
|
break
|
||||||
|
key = [k for k in keys if k not in revoked_keys]
|
||||||
|
return key, list(revoked_keys)
|
Loading…
Reference in a new issue