Release 0.1.3: Compatibility with gpg-wks-client
This commit is contained in:
parent
89542ee110
commit
e15f6e51b1
10 changed files with 127 additions and 42 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -1,5 +1,28 @@
|
|||
# EasyWKS Changelog
|
||||
|
||||
<!-- BEGIN RELEASE v0.1.3 -->
|
||||
## Version 0.1.3
|
||||
|
||||
Compatibility with gpg-wks-client.
|
||||
|
||||
### Changes
|
||||
|
||||
<!-- BEGIN CHANGES 0.1.3 -->
|
||||
- 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.
|
||||
<!-- END CHANGES 0.1.3 -->
|
||||
|
||||
<!-- END RELEASE v0.1.3 -->
|
||||
|
||||
<!-- BEGIN RELEASE v0.1.2 -->
|
||||
## Version 0.1.2
|
||||
|
||||
|
|
32
README.md
32
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!
|
||||
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@<domain> and no password protection.
|
||||
example.com: {}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
|
||||
__version__ = '0.1.2'
|
||||
__version__ = '0.1.3'
|
||||
|
|
4
easywks/__main__.py
Normal file
4
easywks/__main__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
from .main import main
|
||||
|
||||
main()
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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!
|
||||
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@<domain> 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"
|
|
@ -7,6 +7,7 @@ smtp:
|
|||
lmtpd:
|
||||
host: "::"
|
||||
port: 24
|
||||
httpd:
|
||||
host: "::"
|
||||
port: 80
|
||||
domains: {}
|
||||
|
|
Loading…
Reference in a new issue