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
|
# 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 -->
|
<!-- BEGIN RELEASE v0.1.2 -->
|
||||||
## Version 0.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
|
```yaml
|
||||||
---
|
---
|
||||||
# EasyWKS works inside this directory. Its PGP keys as well
|
# EasyWKS works inside this directory. Its PGP keys as well# as all
|
||||||
# as all the submitted and published keys are stored here.
|
# the submitted and published keys are stored here.
|
||||||
directory: /var/lib/easywks
|
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
|
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!
|
# HTTPS-terminating reverse proxy!
|
||||||
|
httpd:
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
# Defaults to stdout, supported: stdout, smtp
|
# Defaults to stdout, supported: stdout, smtp
|
||||||
mailing_method: smtp
|
mailing_method: smtp
|
||||||
|
|
||||||
# Configure smtp client options
|
# Configure smtp client options
|
||||||
smtp:
|
smtp:
|
||||||
# Connect to this SMTP server to send a mail.
|
# Connect to this SMTP server to send a mail.
|
||||||
|
@ -105,13 +116,12 @@ lmtpds:
|
||||||
# Every domain served by EasyWKS must be listed here
|
# Every domain served by EasyWKS must be listed here
|
||||||
domains:
|
domains:
|
||||||
example.org:
|
example.org:
|
||||||
# Users send their requests to this address. It's up to
|
# Users send their requests to this address. It's up to you to
|
||||||
# you to make sure that the mails sent their get handed
|
# make sure that the mails sent their get handed to EasyWKS.
|
||||||
# to EasyWKS.
|
|
||||||
submission_address: webkey@example.org
|
submission_address: webkey@example.org
|
||||||
# If you want the PGP key for this domain to be
|
# If you want the PGP key for this domain to be password-
|
||||||
# password-protected, or if you're supplying your own
|
# protected, or if you're supplying your own password-protected
|
||||||
# password-protected key, set the passphrase here:
|
# key, set the passphrase here:
|
||||||
passphrase: "Correct Horse Battery Staple"
|
passphrase: "Correct Horse Battery Staple"
|
||||||
# Defaults are gpgwks@<domain> and no password protection.
|
# Defaults are gpgwks@<domain> and no password protection.
|
||||||
example.com: {}
|
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
|
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):
|
def _validate_lmtpd_config(value):
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
return f'must be a map, got {type(value)}'
|
return f'must be a map, got {type(value)}'
|
||||||
|
@ -127,10 +136,13 @@ class _GlobalConfig(_Config):
|
||||||
|
|
||||||
Config = _GlobalConfig(
|
Config = _GlobalConfig(
|
||||||
working_directory=_ConfigOption('directory', str, '/var/lib/easywks'),
|
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),
|
pending_lifetime=_ConfigOption('pending_lifetime', int, 604800),
|
||||||
mailing_method=_ConfigOption('mailing_method', str, 'stdout', validator=_validate_mailing_method),
|
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, {
|
smtp=_ConfigOption('smtp', dict, {
|
||||||
'host': 'localhost',
|
'host': 'localhost',
|
||||||
'port': 25,
|
'port': 25,
|
||||||
|
|
|
@ -37,7 +37,7 @@ def parse_arguments():
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parse_arguments()
|
args = parse_arguments()
|
||||||
if args.config is list:
|
if isinstance(args.config, list):
|
||||||
conf = args.config[0]
|
conf = args.config[0]
|
||||||
else:
|
else:
|
||||||
conf = args.config
|
conf = args.config
|
||||||
|
|
|
@ -52,12 +52,12 @@ def _get_pgp_publickey(parts: List[MIMEPart]) -> PGPKey:
|
||||||
key, _ = PGPKey.from_blob(part.get_content())
|
key, _ = PGPKey.from_blob(part.get_content())
|
||||||
if key.is_public:
|
if key.is_public:
|
||||||
if pubkey:
|
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
|
pubkey = key
|
||||||
except PGPError:
|
except PGPError:
|
||||||
pass
|
pass
|
||||||
if not pubkey:
|
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
|
return pubkey
|
||||||
|
|
||||||
|
|
||||||
|
@ -90,8 +90,6 @@ def _find_confirmation_response(parts: List[MIMEPart]) -> str:
|
||||||
if 'confirmation-response' in c:
|
if 'confirmation-response' in c:
|
||||||
response = c
|
response = c
|
||||||
continue
|
continue
|
||||||
if not response:
|
|
||||||
raise EasyWksError('No confirmation response found in message')
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,12 +103,19 @@ def _parse_confirmation_response(pgp: PGPMessage, submission: str, sender: str):
|
||||||
continue
|
continue
|
||||||
key, value = line.split(':', 1)
|
key, value = line.split(':', 1)
|
||||||
rdict[key.strip()] = value.strip()
|
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':
|
if rdict.get('type', '') != 'confirmation-response':
|
||||||
raise EasyWksError('Invalid confirmation response: "type" missing or not "confirmation-response"')
|
raise EasyWksError('Invalid confirmation response: "type" missing or not "confirmation-response"')
|
||||||
if 'sender' not in rdict or 'nonce' not in rdict:
|
if 'sender' not in rdict or 'nonce' not in rdict:
|
||||||
raise EasyWksError('Invalid confirmation response: "sender" or "nonce" missing from confirmation response')
|
raise EasyWksError('Invalid confirmation response: "sender" or "nonce" missing from confirmation response')
|
||||||
if rdict['sender'] != sender:
|
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)
|
return ConfirmationResponse(rdict['sender'], submission, rdict['nonce'], pgp)
|
||||||
|
|
||||||
|
|
||||||
|
@ -138,7 +143,12 @@ def process_mail(mail: bytes):
|
||||||
leafs = _get_mime_leafs(msg)
|
leafs = _get_mime_leafs(msg)
|
||||||
pgp: PGPMessage = _get_pgp_message(leafs)
|
pgp: PGPMessage = _get_pgp_message(leafs)
|
||||||
decrypted = pgp_decrypt(sender_domain, pgp)
|
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)
|
request: ConfirmationResponse = _parse_confirmation_response(decrypted, submission_address, sender_mail)
|
||||||
try:
|
try:
|
||||||
key = read_pending_key(sender_domain, request.nonce)
|
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. '
|
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.')
|
||||||
# this throws an error if signature verification fails
|
# 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()
|
rmsg = response.create_signed_message()
|
||||||
write_public_key(sender_domain, sender_mail, key)
|
write_public_key(sender_domain, sender_mail, key)
|
||||||
remove_pending_key(sender_domain, request.nonce)
|
remove_pending_key(sender_domain, request.nonce)
|
||||||
|
|
|
@ -10,6 +10,7 @@ from pgpy import PGPKey, PGPMessage, PGPUID
|
||||||
from pgpy.types import SignatureVerification
|
from pgpy.types import SignatureVerification
|
||||||
|
|
||||||
from .crypto import pgp_sign
|
from .crypto import pgp_sign
|
||||||
|
from .config import Config
|
||||||
from .util import create_nonce, fingerprint
|
from .util import create_nonce, fingerprint
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,15 +158,26 @@ class ConfirmationResponse:
|
||||||
def nonce(self):
|
def nonce(self):
|
||||||
return self._nonce
|
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)
|
uid: PGPUID = key.get_uid(self._submitter_addr)
|
||||||
if uid is None or uid.email != 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')
|
raise EasyWksError(f'UID {self._submitter_addr} not found in PGP key')
|
||||||
verification: SignatureVerification = key.verify(self._msg)
|
verification: SignatureVerification = key.verify(self._msg)
|
||||||
for verified, by, sig, subject in verification.good_signatures:
|
for verified, by, sig, subject in verification.good_signatures:
|
||||||
if fingerprint(key) == fingerprint(by):
|
if fingerprint(key) == fingerprint(by):
|
||||||
return PublishResponse(self._submitter_addr, self._submission_addr, key)
|
return
|
||||||
raise EasyWksError('PGP Signature could not be verified')
|
raise EasyWksError('PGP signature could not be verified')
|
||||||
|
|
||||||
|
|
||||||
class PublishResponse:
|
class PublishResponse:
|
||||||
|
@ -214,7 +226,7 @@ Encrypt live everybody is.
|
||||||
return self._domain
|
return self._domain
|
||||||
|
|
||||||
def create_signed_message(self):
|
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')
|
_subtype='plain')
|
||||||
to_encrypt = PGPMessage.new(mpplain.as_string(policy=default))
|
to_encrypt = PGPMessage.new(mpplain.as_string(policy=default))
|
||||||
encrypted: PGPMessage = self.key.encrypt(to_encrypt)
|
encrypted: PGPMessage = self.key.encrypt(to_encrypt)
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
---
|
---
|
||||||
# EasyWKS works inside this directory. Its PGP keys as well
|
# EasyWKS works inside this directory. Its PGP keys as well as all the
|
||||||
# as all the submitted and published keys are stored here.
|
# submitted and published keys are stored here.
|
||||||
#directory: /var/lib/easywks
|
#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
|
#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
|
# Port configuration for the webserver. Put this behind a
|
||||||
# HTTPS-terminating reverse proxy!
|
# HTTPS-terminating reverse proxy!
|
||||||
|
httpd:
|
||||||
host: "::1"
|
host: "::1"
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
# Defaults to stdout, supported: stdout, smtp
|
# Defaults to stdout, supported: stdout, smtp
|
||||||
mailing_method: smtp
|
mailing_method: smtp
|
||||||
|
|
||||||
# Configure smtp client options
|
# Configure smtp client options
|
||||||
smtp:
|
smtp:
|
||||||
# Connect to this SMTP server to send a mail.
|
# Connect to this SMTP server to send a mail.
|
||||||
|
@ -22,19 +33,20 @@ smtp:
|
||||||
# Omit username/password if authentication is not needed.
|
# Omit username/password if authentication is not needed.
|
||||||
#username: webkey
|
#username: webkey
|
||||||
#password: SuperS3curePassword123
|
#password: SuperS3curePassword123
|
||||||
|
|
||||||
# Configure the LMTP server
|
# Configure the LMTP server
|
||||||
lmtpd:
|
lmtpd:
|
||||||
host: "::1"
|
host: "::1"
|
||||||
port: 8024
|
port: 8024
|
||||||
|
|
||||||
# Every domain served by EasyWKS must be listed here
|
# Every domain served by EasyWKS must be listed here
|
||||||
domains:
|
domains:
|
||||||
# Defaults are gpgwks@<domain> and no password protection.
|
# Defaults are gpgwks@<domain> and no password protection.
|
||||||
example.org:
|
example.org:
|
||||||
# Users send their requests to this address. It's up to
|
# Users send their requests to this address. It's up to you to
|
||||||
# you to make sure that the mails sent their get handed
|
# make sure that the mails sent their get handed to EasyWKS.
|
||||||
# to EasyWKS.
|
|
||||||
submission_address: webkey@example.org
|
submission_address: webkey@example.org
|
||||||
# If you want the PGP key for this domain to be
|
# If you want the PGP key for this domain to be password-protected,
|
||||||
# password-protected, or if you're supplying your own
|
# or if you're supplying your own password-protected key, set the
|
||||||
# password-protected key, set the passphrase here:
|
# passphrase here:
|
||||||
#passphrase: "Correct Horse Battery Staple"
|
#passphrase: "Correct Horse Battery Staple"
|
|
@ -7,6 +7,7 @@ smtp:
|
||||||
lmtpd:
|
lmtpd:
|
||||||
host: "::"
|
host: "::"
|
||||||
port: 24
|
port: 24
|
||||||
|
httpd:
|
||||||
host: "::"
|
host: "::"
|
||||||
port: 80
|
port: 80
|
||||||
domains: {}
|
domains: {}
|
||||||
|
|
Loading…
Reference in a new issue