diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bbee18..5141201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # EasyWKS Changelog + +## Version 0.1.3 + +Compatibility with gpg-wks-client. + +### Changes + + +- Webserver config is now under a `httpd` key, to be more in line with + lmtpd and smtp. +- Add a `permit_unsigned_response` boolean key that, if set to true, + instructs EasyWKS to accept confirmation responses even if they are + unsigned, in order to be compatible with version -00 of the draft + standard, and thus e.g. gpg-wks-client. +- Change the detection logic between submission requests and + confirmation requests from PGP signature checking to attempting to + parse the message as a submission response first. +- Add a `__main__` module so that easywks can be invoked as a Python + module. + + + + ## Version 0.1.2 diff --git a/README.md b/README.md index 71052dc..37f26f0 100644 --- a/README.md +++ b/README.md @@ -75,18 +75,29 @@ Configuration is done in `/etc/easywks.yml` (or any other place as specified by ```yaml --- -# EasyWKS works inside this directory. Its PGP keys as well -# as all the submitted and published keys are stored here. +# EasyWKS works inside this directory. Its PGP keys as well# as all +# the submitted and published keys are stored here. directory: /var/lib/easywks -# Number of seconds after which a pending submission request -# is considered stale and should be removed by easywks clean. + +# Number of seconds after which a pending submission request is +# considered stale and should be removed by easywks clean. pending_lifetime: 604800 -# Port configuration for the webserver. Put this behind a + +# Some clients (including recent versions of gpg-wks-client follow an +# older version of the WKS standard where signing the confirmation +# response is only recommended, but not required. Set this option to +# true if you want to accept such unsigned responses. +permit_unsigned_response: false + +# Port configuration for the webserver. Put this behind an # HTTPS-terminating reverse proxy! -host: 127.0.0.1 -port: 8080 +httpd: + host: 127.0.0.1 + port: 8080 + # Defaults to stdout, supported: stdout, smtp mailing_method: smtp + # Configure smtp client options smtp: # Connect to this SMTP server to send a mail. @@ -105,13 +116,12 @@ lmtpds: # Every domain served by EasyWKS must be listed here domains: example.org: - # Users send their requests to this address. It's up to - # you to make sure that the mails sent their get handed - # to EasyWKS. + # Users send their requests to this address. It's up to you to + # make sure that the mails sent their get handed to EasyWKS. submission_address: webkey@example.org - # If you want the PGP key for this domain to be - # password-protected, or if you're supplying your own - # password-protected key, set the passphrase here: + # If you want the PGP key for this domain to be password- + # protected, or if you're supplying your own password-protected + # key, set the passphrase here: passphrase: "Correct Horse Battery Staple" # Defaults are gpgwks@ and no password protection. example.com: {} diff --git a/easywks/__init__.py b/easywks/__init__.py index db8088a..9dd5eb4 100644 --- a/easywks/__init__.py +++ b/easywks/__init__.py @@ -1,2 +1,2 @@ -__version__ = '0.1.2' +__version__ = '0.1.3' diff --git a/easywks/__main__.py b/easywks/__main__.py new file mode 100644 index 0000000..ac23724 --- /dev/null +++ b/easywks/__main__.py @@ -0,0 +1,4 @@ + +from .main import main + +main() diff --git a/easywks/config.py b/easywks/config.py index 34b9e63..0dca122 100644 --- a/easywks/config.py +++ b/easywks/config.py @@ -36,6 +36,15 @@ def _validate_smtp_config(value): value['password'] = None +def _validate_httpd_config(value): + if not isinstance(value, dict): + return f'must be a map, got {type(value)}' + if not isinstance(value['host'], str): + return f'host must be a str, got {type(value["host"])}' + if not isinstance(value['port'], int): + return f'port must be a int, got {type(value["port"])}' + + def _validate_lmtpd_config(value): if not isinstance(value, dict): return f'must be a map, got {type(value)}' @@ -127,10 +136,13 @@ class _GlobalConfig(_Config): Config = _GlobalConfig( working_directory=_ConfigOption('directory', str, '/var/lib/easywks'), - host=_ConfigOption('host', str, '127.0.0.1'), - port=_ConfigOption('port', int, 8080), pending_lifetime=_ConfigOption('pending_lifetime', int, 604800), mailing_method=_ConfigOption('mailing_method', str, 'stdout', validator=_validate_mailing_method), + permit_unsigned_response=_ConfigOption('permit_unsigned_response', bool, False), + httpd=_ConfigOption('httpd', dict, { + 'host': 'localhost', + 'port': 8080 + }, validator=_validate_httpd_config), smtp=_ConfigOption('smtp', dict, { 'host': 'localhost', 'port': 25, diff --git a/easywks/main.py b/easywks/main.py index 8d9f0b9..626705d 100644 --- a/easywks/main.py +++ b/easywks/main.py @@ -37,7 +37,7 @@ def parse_arguments(): def main(): args = parse_arguments() - if args.config is list: + if isinstance(args.config, list): conf = args.config[0] else: conf = args.config diff --git a/easywks/process.py b/easywks/process.py index 7b089b8..5e02ce8 100644 --- a/easywks/process.py +++ b/easywks/process.py @@ -52,12 +52,12 @@ def _get_pgp_publickey(parts: List[MIMEPart]) -> PGPKey: key, _ = PGPKey.from_blob(part.get_content()) if key.is_public: if pubkey: - raise EasyWksError('More than one PGP public key in message') + raise EasyWksError('More than one PGP public key in message. Only submit a single key at once.') pubkey = key except PGPError: pass if not pubkey: - raise EasyWksError('No PGP public key in message') + raise EasyWksError('No PGP public key found in the encrypted message part.') return pubkey @@ -90,8 +90,6 @@ def _find_confirmation_response(parts: List[MIMEPart]) -> str: if 'confirmation-response' in c: response = c continue - if not response: - raise EasyWksError('No confirmation response found in message') return response @@ -105,12 +103,19 @@ def _parse_confirmation_response(pgp: PGPMessage, submission: str, sender: str): continue key, value = line.split(':', 1) rdict[key.strip()] = value.strip() + # "from" was renamed to "sender" in draft-koch-openpgp-webkey-service-01. In addition, gpg-wks-client, which still + # implements -00, also violates this standard and uses "address" for the sender and "sender" for the submission + # address. + if 'from' in rdict: + rdict['sender'] = rdict['from'] + if 'address' in rdict: + rdict['sender'] = rdict['address'] if rdict.get('type', '') != 'confirmation-response': raise EasyWksError('Invalid confirmation response: "type" missing or not "confirmation-response"') if 'sender' not in rdict or 'nonce' not in rdict: raise EasyWksError('Invalid confirmation response: "sender" or "nonce" missing from confirmation response') if rdict['sender'] != sender: - raise EasyWksError('Confirmation sender does not match message sender') + raise EasyWksError(f'Confirmation sender "{rdict["sender"]}" does not match message sender "{sender}"') return ConfirmationResponse(rdict['sender'], submission, rdict['nonce'], pgp) @@ -138,7 +143,12 @@ def process_mail(mail: bytes): leafs = _get_mime_leafs(msg) pgp: PGPMessage = _get_pgp_message(leafs) decrypted = pgp_decrypt(sender_domain, pgp) - if decrypted.is_signed: + payload = BytesParser(policy=default).parsebytes(decrypted.message) + parts = _get_mime_leafs(payload) + # First attempt to find a confirmation response. It's identifiable much easier due to the + # "type: confirmation-response" part in the message. + confirmation_response = _find_confirmation_response(parts) + if confirmation_response is not None: request: ConfirmationResponse = _parse_confirmation_response(decrypted, submission_address, sender_mail) try: key = read_pending_key(sender_domain, request.nonce) @@ -146,7 +156,8 @@ def process_mail(mail: bytes): raise EasyWksError('There is no submission request for this email address, or it has expired. ' 'Please resubmit your submission request.') # this throws an error if signature verification fails - response: PublishResponse = request.verify_signature(key) + request.verify_signature(key) + response: PublishResponse = request.get_publish_response(key) rmsg = response.create_signed_message() write_public_key(sender_domain, sender_mail, key) remove_pending_key(sender_domain, request.nonce) diff --git a/easywks/types.py b/easywks/types.py index fb9c46e..9f63d27 100644 --- a/easywks/types.py +++ b/easywks/types.py @@ -10,6 +10,7 @@ from pgpy import PGPKey, PGPMessage, PGPUID from pgpy.types import SignatureVerification from .crypto import pgp_sign +from .config import Config from .util import create_nonce, fingerprint @@ -157,15 +158,26 @@ class ConfirmationResponse: def nonce(self): return self._nonce - def verify_signature(self, key: PGPKey) -> 'PublishResponse': + def get_publish_response(self, key: PGPKey) -> 'PublishResponse': + return PublishResponse(self._submitter_addr, self._submission_addr, key) + + def verify_signature(self, key: PGPKey): + if not self._msg.is_signed: + if not Config.permit_unsigned_response: + raise EasyWksError('The confirmation response is not signed. If you used an automated tool such as ' + 'gpg-wks-client for submitting your response, please update said tool or try ' + 'responding manually.') + else: + # Unsigned, but permitted + return uid: PGPUID = key.get_uid(self._submitter_addr) if uid is None or uid.email != self._submitter_addr: raise EasyWksError(f'UID {self._submitter_addr} not found in PGP key') verification: SignatureVerification = key.verify(self._msg) for verified, by, sig, subject in verification.good_signatures: if fingerprint(key) == fingerprint(by): - return PublishResponse(self._submitter_addr, self._submission_addr, key) - raise EasyWksError('PGP Signature could not be verified') + return + raise EasyWksError('PGP signature could not be verified') class PublishResponse: @@ -214,7 +226,7 @@ Encrypt live everybody is. return self._domain def create_signed_message(self): - mpplain = MIMEText(ConfirmationRequest.MAIL_TEXT.format(domain=self.domain, uid=self.submitter_address), + mpplain = MIMEText(PublishResponse.MAIL_TEXT.format(domain=self.domain, uid=self.submitter_address), _subtype='plain') to_encrypt = PGPMessage.new(mpplain.as_string(policy=default)) encrypted: PGPMessage = self.key.encrypt(to_encrypt) diff --git a/package/debian/easywks/etc/easywks.yml b/package/debian/easywks/etc/easywks.yml index 6895a64..2ea0e35 100644 --- a/package/debian/easywks/etc/easywks.yml +++ b/package/debian/easywks/etc/easywks.yml @@ -1,16 +1,27 @@ --- -# EasyWKS works inside this directory. Its PGP keys as well -# as all the submitted and published keys are stored here. +# EasyWKS works inside this directory. Its PGP keys as well as all the +# submitted and published keys are stored here. #directory: /var/lib/easywks -# Number of seconds after which a pending submission request -# is considered stale and should be removed by easywks clean. + +# Number of seconds after which a pending submission request is +# considered stale and should be removed by easywks clean. #pending_lifetime: 604800 + +# Some clients (including recent versions of gpg-wks-client follow an +# older version of the WKS standard where signing the confirmation +# response is only recommended, but not required. Set this option to +# true if you want to accept such unsigned responses. +#permit_unsigned_response: false + # Port configuration for the webserver. Put this behind a # HTTPS-terminating reverse proxy! -host: "::1" -port: 8080 +httpd: + host: "::1" + port: 8080 + # Defaults to stdout, supported: stdout, smtp mailing_method: smtp + # Configure smtp client options smtp: # Connect to this SMTP server to send a mail. @@ -22,19 +33,20 @@ smtp: # Omit username/password if authentication is not needed. #username: webkey #password: SuperS3curePassword123 + # Configure the LMTP server lmtpd: host: "::1" port: 8024 + # Every domain served by EasyWKS must be listed here domains: # Defaults are gpgwks@ and no password protection. example.org: - # Users send their requests to this address. It's up to - # you to make sure that the mails sent their get handed - # to EasyWKS. + # Users send their requests to this address. It's up to you to + # make sure that the mails sent their get handed to EasyWKS. submission_address: webkey@example.org - # If you want the PGP key for this domain to be - # password-protected, or if you're supplying your own - # password-protected key, set the passphrase here: + # If you want the PGP key for this domain to be password-protected, + # or if you're supplying your own password-protected key, set the + # passphrase here: #passphrase: "Correct Horse Battery Staple" \ No newline at end of file diff --git a/package/docker/easywks.yml b/package/docker/easywks.yml index b15e62c..359af34 100644 --- a/package/docker/easywks.yml +++ b/package/docker/easywks.yml @@ -7,6 +7,7 @@ smtp: lmtpd: host: "::" port: 24 -host: "::" -port: 80 +httpd: + host: "::" + port: 80 domains: {}